This commit is contained in:
oneao committed 2025-12-05 17:28:46 +08:00
1 parent 33ab76f73e
commit 3af982d669
31 files changed
+632 -316

No files matched your search

@@ -12,6 +12,7 @@ import { LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { useUserStore } from '@/store/user'
interface StaticTransactionsProps {
records: MoneyRecordGroup[]
@@ -30,7 +31,7 @@ export default function StaticTransactions({
onLoadMore,
onRefreshData,
}: StaticTransactionsProps) {
const moneyCategoryStore = useMoneyCategoryStore()
const userID = useUserStore().userInfo?.id
const [refreshing, setRefreshing] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [selectedId, setSelectedId] = useState<number | null>(null)
@@ -119,7 +120,6 @@ export default function StaticTransactions({
)}
renderItem={({ item, index }) => {
const category = moneyCategoryStore.getCategoryById(item.category_id)
return (
<Menu
renderer={renderers.Popover}
@@ -147,28 +147,27 @@ export default function StaticTransactions({
}}
>
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
<MoneyCategoryIcon name={category?.icon} size={20} color="#000" />
<MoneyCategoryIcon name={item.category_icon} size={20} color="#000" />
</View>
<View className="flex-1">
<Text className="text-gray-800 text-base">
{item.remark ? item.remark : category?.name}
{item.remark ? item.remark : item.category_name}
</Text>
{
index === 1 && (
item.user_id !== userID && (
<Text className="text-gray-500 text-xs italic">
张三
{item.username}
</Text>
)
}
</View>
<View>
<Text
className="text-sm text-gray-600 text-right"
>
{category?.type === 1 ? '+' : '-'}
{item.category_type === 1 ? '+' : '-'}
{' '}
{item.amount}
</Text>
@@ -30,6 +30,8 @@ export default function Money() {
}).then((res) => {
if (res.isSuccess) {
moneyRecordStore.setRecords(res.data.records, res.data.total_expense, res.data.total_income, searchParams.record_time)
console.log(res.data.records)
}
else {
toast.show({
@@ -1,13 +1,16 @@
import { Text, TouchableOpacity, View } from 'react-native'
import { Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { LucideIcon } from '@/components/common/ao/index'
import {
Avatar,
AvatarImage,
} from '@/components/ui/avatar'
import { useUserStore } from '@/store/user'
import ListItem from './components/ListItem'
export default function User() {
const userStore = useUserStore()
return (
<SafeAreaView className="flex-1 p-4">
<View className="flex flex-row items-center ">
@@ -17,7 +20,7 @@ export default function User() {
source={require('@/static/images/logo.png')}
/>
</Avatar>
<Text className="text-xl font-semibold ml-3">张三</Text>
<Text className="text-xl font-semibold ml-3">{userStore.userInfo?.username}</Text>
<LucideIcon icon="ChevronRight" size={26} className="ml-auto" color="#aaa" strokeWidth={1.5} />
</View>
@@ -11,6 +11,7 @@ import { MenuProvider } from 'react-native-popup-menu'
import { AButton } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
import { getUserSpaceApi } from '@/request/api/space/spaceUser'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { useNavStore } from '@/store/navigation'
import { useSpaceStore } from '@/store/space'
@@ -70,7 +71,12 @@ export default function RootLayout() {
// 如果登录的话,就初始化下
moneyCategoryStore.init()
spaceStore.init()
getUserSpaceApi({}).then((res) => {
spaceStore.setUserSpace(res.data)
}).catch((err) => {
console.log(err)
})
// 隐藏 splash
SplashScreen.hideAsync()
@@ -4,18 +4,15 @@ import { Text, View } from 'react-native'
import { AButton, ACell, ACellGroup, AModal } from '@/components/common/ao'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import CustomHeader from '@/components/custom/Header'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { useMoneyRecordStore } from '@/store/moneyRecord'
import { toDateString, toDateTimeString } from '@/utils/time'
export default function MoneyRecordDetail() {
const router = useRouter()
const moneyRecordStore = useMoneyRecordStore()
const moneyCategoryStore = useMoneyCategoryStore()
const { id: detailId } = useLocalSearchParams<{ id: string }>()
const moneryRecord = moneyRecordStore.getMoneyRecordById(Number(detailId))
const moneyCategory = moneyCategoryStore.getCategoryById(moneryRecord?.category_id || 0)
const [showDeleteModal, setShowDeleteModal] = useState(false)
@@ -29,12 +26,12 @@ export default function MoneyRecordDetail() {
<CustomHeader title="账单详情" />
<View className="flex-1">
<View className="flex flex-col items-center">
<MoneyCategoryIcon name={moneyCategory?.icon} size={56} />
<Text className="mt-1">{moneyCategory?.name}</Text>
<MoneyCategoryIcon name={moneryRecord.category_icon} size={56} />
<Text className="mt-1">{moneryRecord.category_name}</Text>
</View>
<ACellGroup showDivider={false} className="mt-4">
<ACell title="类别" rightText={moneyCategory?.type === 0 ? '支出' : '收入'} />
<ACell title="类别" rightText={moneryRecord.category_type === 0 ? '支出' : '收入'} />
<ACell title="金额" rightText={moneryRecord.amount} />
<ACell title="备注" rightText={moneryRecord.remark} />
<ACell title="日期" rightText={toDateString(moneryRecord.record_time)} />
@@ -31,6 +31,10 @@ export default function MoenyRecordEditor() {
const defaultMoneyRecord: MoneyRecordBase = {
id: -1,
category_id: -1,
username: '',
category_icon: '',
category_name: '',
category_type: 0,
user_id: -1,
amount: '0.00',
remark: '',
@@ -33,7 +33,7 @@ export default function Share() {
style={{
backgroundColor: '#d9d9db',
}}
onPress={() => setShowExitModal(true)}
onPress={() => exitLogin()}
>
<Text>退出登录</Text>
</TouchableOpacity>
@@ -1,4 +1,3 @@
import type { UserSpace } from '@/types/space/spaceUser'
import { Text, View } from 'react-native'
import { AButton, ADivider, LucideIcon } from '@/components/common/ao'
@@ -1,4 +1,4 @@
import type { SpaceUser, UserSpace } from '@/types/space/spaceUser'
import type { SpaceUser } from '@/types/space/spaceUser'
import * as Clipboard from 'expo-clipboard'
import { useState } from 'react'
import { FlatList, TouchableHighlight, TouchableOpacity, View } from 'react-native'
@@ -32,19 +32,29 @@ import { HStack } from '@/components/ui/hstack'
import { ChevronDownIcon, ChevronUpIcon } from '@/components/ui/icon'
import { Text } from '@/components/ui/text'
import { VStack } from '@/components/ui/vstack'
import { exitSpaceApi } from '@/request/api/space/spaceUser'
import { dissolveSpaceApi } from '@/request/api/space/space'
import { exitSpaceApi, removeSpaceUserApi } from '@/request/api/space/spaceUser'
import { useUserStore } from '@/store/user'
import { formatRelativeTime } from '@/utils/time'
import { toast } from '@/utils/toast'
type ExitAction = 'exit' | 'remove' | 'dissolve'
interface ExitParams {
type: ExitAction
userId?: number
}
interface SpaceInviteProps {
userSpace: UserSpace | null
spaceUsers: SpaceUser[]
onExit?: (params: ExitParams) => void
}
export default function SpaceInvite({
userSpace,
spaceUsers,
onExit,
}: SpaceInviteProps) {
const userStore = useUserStore()
@@ -52,13 +62,14 @@ export default function SpaceInvite({
const [modal, setModal] = useState<{
open: boolean
title: string
type: 'exit' | 'remove' | 'dissolve'
type: ExitAction
userId?: number
}>({
open: false,
title: '',
type: 'dissolve',
})
,
const [accordion, setAccordion] = useState(['a'])
async function copyInviteCode() {
@@ -69,8 +80,43 @@ export default function SpaceInvite({
})
}
function exitSpace() {
function handleModalConfirm() {
if (userSpace === null) {
return
}
if (modal.type === 'exit') {
// 退出
exitSpaceApi({}).catch((err) => {
console.log(err)
})
}
else if (modal.type === 'remove') {
if (!modal.userId) {
return
}
// 移除
removeSpaceUserApi({
user_id: modal.userId,
}).catch((err) => {
console.log(err)
})
}
else {
// 解散
dissolveSpaceApi({}).catch((err) => {
console.log(err)
})
}
setModal({
...modal,
open: false,
})
onExit?.({
type: modal.type,
userId: modal.userId,
})
}
return (
@@ -207,36 +253,31 @@ export default function SpaceInvite({
},
}}
>
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0',
activeOpacity: 0.7,
},
}}
onSelect={() => {
}}
>
<View className="flex flex-row items-center h-10 px-2">
<Text className="text-lg">授予管理员</Text>
</View>
</MenuOption>
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0',
activeOpacity: 0.7,
},
}}
onSelect={() => {
}}
>
<View className="flex flex-row items-center h-10 px-2">
<Text className="text-lg">踢出空间</Text>
</View>
</MenuOption>
{userSpace.role === 0 && userStore.userInfo?.id !== item.user_id && (
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0',
activeOpacity: 0.7,
},
}}
onSelect={() => {
setModal({
...modal,
open: true,
title: `确定将 ${item.username} 提出空间?`,
type: 'remove',
userId: item.user_id,
})
}}
>
<View className="flex flex-row items-center h-10 px-2">
<Text className="text-lg">踢出空间</Text>
</View>
</MenuOption>
)}
</MenuOptions>
</Menu>
</View>
@@ -247,15 +288,50 @@ export default function SpaceInvite({
)}
/>
</View>
<AButton size="sm" className="mb-8 mt-4">退出该空间</AButton>
<AButton
size="sm"
className="mb-8 mt-4"
onPress={() => {
if (userSpace?.role === 0) {
setModal({
...modal,
open: true,
title: `确定解散该空间么?不可恢复!`,
type: 'dissolve',
})
}
else {
setModal({
...modal,
open: true,
title: `确定退出该空间么?`,
type: 'exit',
})
}
}}
>
{userSpace?.role === 0 ? '解散空间' : '退出空间'}
</AButton>
</View>
<AModal
title="确认退出当前账号?"
isOpen={showExitModal}
onCancel={() => setShowExitModal(false)}
onClose={() => setShowExitModal(false)}
onConfirm={exitLogin}
title={modal.title}
isOpen={modal.open}
onCancel={() => {
setModal({
...modal,
open: false,
})
}}
onClose={() => {
setModal({
...modal,
open: false,
})
}}
onConfirm={() => {
handleModalConfirm()
}}
/>
</View>
)
@@ -1,17 +1,19 @@
import type { SpaceUser, UserSpace } from '@/types/space/spaceUser'
import type { SpaceUser } 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 { useSpaceStore } from '@/store/space'
import JoinSpace from './components/JoinSpace'
import SpaceHome from './components/SpaceHome'
import SpaceInviteAndMembers from './components/SpaceInvite'
import SpaceInvite from './components/SpaceInvite'
import StepFade from './components/StepFade'
export default function PagerWithTab() {
const spaceStore = useSpaceStore()
const [step, setStep] = useState<'join' | 'invite' | 'home'>('home')
const [userSpace, setUserSpace] = useState<UserSpace | null>(null)
const [spaceUsers, setSpaceUsers] = useState<SpaceUser[]>([])
const fetchData = async () => {
@@ -20,11 +22,11 @@ export default function PagerWithTab() {
.then((res) => {
if (res.isSuccess) {
if (res.data === null) {
setUserSpace(null)
spaceStore.setUserSpace(null)
setStep('home')
}
else {
setUserSpace({
spaceStore.setUserSpace({
space_id: res.data.space_id,
space_name: res.data.space_name,
role: res.data.role,
@@ -51,6 +53,7 @@ export default function PagerWithTab() {
.catch(err => console.error('listSpaceUsersApi error:', err)),
])
}
useEffect(() => {
fetchData()
}, [])
@@ -58,13 +61,22 @@ export default function PagerWithTab() {
function createSpace() {
createSpaceApi({}).then((res) => {
if (res.isSuccess) {
setUserSpace({
spaceStore.setUserSpace({
space_id: res.data.id,
space_name: '',
role: 0,
invite_code: res.data.invite_code,
})
listSpaceUsersApi({})
.then((res) => {
if (res.isSuccess) {
setSpaceUsers(res.data.users)
}
else {
// 提示
}
})
.catch(err => console.error('listSpaceUsersApi error:', err))
setStep('invite')
}
}).catch((err) => {
@@ -111,7 +123,21 @@ export default function PagerWithTab() {
</StepFade>
<StepFade visible={step === 'invite'}>
<SpaceInviteAndMembers userSpace={userSpace} spaceUsers={spaceUsers} />
<SpaceInvite
userSpace={spaceStore.userSpace}
spaceUsers={spaceUsers}
onExit={(data) => {
if (data.type === 'remove' && data.userId !== undefined) {
setSpaceUsers(prevUsers =>
prevUsers.filter(user => user.user_id !== data.userId),
)
return
}
setStep('home')
spaceStore.setUserSpace(null)
setSpaceUsers([])
}}
/>
</StepFade>
</View>
</View>
@@ -9,8 +9,6 @@ import type {
ListSpaceUsersResult,
RemoveSpaceUserParams,
RemoveSpaceUserResult,
UpdateSpaceUserRoleParams,
UpdateSpaceUserRoleResult,
} from '@/types/space/spaceUser'
import alovaInstance from '../../index'
@@ -35,7 +33,3 @@ export function listSpaceUsersApi(params: ListSpaceUsersParams) {
export function removeSpaceUserApi(params: RemoveSpaceUserParams) {
return alovaInstance.Post<ResponseData<RemoveSpaceUserResult>>(`${API_PREFIX}/remove`, params)
}
export function updateSpaceUserRoleApi(params: UpdateSpaceUserRoleParams) {
return alovaInstance.Post<ResponseData<UpdateSpaceUserRoleResult>>(`${API_PREFIX}/updateRole`, params)
}
@@ -4,6 +4,7 @@ import type { RefreshTokenResult } from '@/types/auth'
import { axiosRequestAdapter } from '@alova/adapter-axios'
import { createAlova } from 'alova'
import { useNavStore } from '@/store/navigation'
import { useSpaceStore } from '@/store/space'
import { useUserStore } from '@/store/user'
import { toast } from '@/utils/toast'
@@ -31,8 +32,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,
@@ -149,6 +150,12 @@ const alovaInstance = createAlova({
}
method.config.headers.token = useUserStore.getState().userInfo?.token ?? ''
const userSpace = useSpaceStore.getState().userSpace
if (userSpace !== null) {
const spaceID = userSpace.space_id
method.config.headers['x-space-id'] = spaceID
}
},
})
@@ -19,15 +19,20 @@ interface MoneyCategoryStore {
updateCategories: (data: MoneyCategoryItem[], type: number) => void
deleteCategoryById: (id: number, type: number) => void
sysCategories: MoneyCategoryItem[]
setSysCategories: (categories: MoneyCategoryItem[]) => void
}
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
persist(
(set, get) => ({
categories: [],
sysCategories:[],
setCategories: categories => set({ categories }),
setSysCategories: categories => set({ categories }),
addCategory: category =>
set({ categories: [...get().categories, category] }),
@@ -35,7 +40,6 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
set({ categories: get().categories.filter(c => c.id !== id) }),
init: async () => {
try {
await queryMoneyCategoryApi().then((res) => {
if (res.isSuccess) {
@@ -58,6 +62,11 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
getCategoryById: (id: number) => {
return get().categories.find(c => c.id === id) || null
},
getSysCategoryById: (id: number) => {
return get().sysCategories.find(c => c.id === id) || null
},
updateCategories: (data, type) => {
// 获取当前所有分类
const allCategories = get().categories
@@ -1,23 +1,11 @@
import { create } from 'zustand'
import { getUserSpaceApi } from '@/request/api/space/spaceUser'
interface SpaceState {
userSpace: UserSpace | null
setUserSpace: (space: UserSpace | null) => void
init: () => Promise<void>
}
export const useSpaceStore = create<SpaceState>(set => ({
userSpace: null,
setUserSpace: space => set({ userSpace: space }),
init: async () => {
try {
const res = await getUserSpaceApi({})
set({ userSpace: res.isSuccess ? res.data : null })
}
catch (err) {
set({ userSpace: null })
console.error(err)
}
},
}))
@@ -4,7 +4,11 @@
export interface MoneyRecordBase {
id: number
category_id: number
category_type: number
category_name: string
category_icon: string
user_id: number
username: string
amount: string
remark: string
record_time: string
@@ -47,7 +51,13 @@ export interface InsertMoneyRecordParams {
export interface InsertMoneyRecordResult extends MoneyRecordBase {}
// 更新
export interface UpdateMoneyRecordParams extends Omit<MoneyRecordBase, 'create_time' | 'user_id'> {}
export interface UpdateMoneyRecordParams {
id: number
category_id: number
amount: string
remark: string
record_time: string
}
// 删除
export interface DeleteMoneyRecordParams {
@@ -1,10 +1,3 @@
export interface UserSpace {
space_id: number
space_name: string
role: number
invite_code: string
}
// 获取用户的空间
export interface GetUserSpaceParams {
@@ -41,7 +34,6 @@ export interface ListSpaceUsersResult {
// 踢出空间
export interface RemoveSpaceUserParams {
space_id: number
user_id: number
}