u
This commit is contained in:
1 parent
3af982d669
commit
6a24be0864
28 files changed
+572
-458
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { AListItem, LucideIcon } from '@/components/common/ao'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerBody,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
} from '@/components/ui/drawer'
|
||||
|
||||
interface ActionsMenuProps {
|
||||
isOpen?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export default function ActionsMenu({
|
||||
isOpen = false,
|
||||
onClose,
|
||||
}: ActionsMenuProps) {
|
||||
const safeAreaInsets = useSafeAreaInsets()
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
anchor="right"
|
||||
size="lg"
|
||||
>
|
||||
<DrawerBackdrop />
|
||||
<DrawerContent
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingLeft: safeAreaInsets.left + 8,
|
||||
paddingRight: safeAreaInsets.right + 8,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require('@/static/images/logo.png')}
|
||||
style={{ width: 40, height: 40 }}
|
||||
/>
|
||||
</DrawerHeader>
|
||||
<DrawerBody
|
||||
style={{
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<View className="mb-3">
|
||||
<Text className="text-gray-600 font-semibold text-sm">常用菜单</Text>
|
||||
|
||||
<View className="p-1">
|
||||
<AListItem icon="LayoutGrid" text="类别设置" />
|
||||
<AListItem icon="ChartArea" text="图表分析" />
|
||||
</View>
|
||||
</View>
|
||||
<View className="mb-3">
|
||||
<Text className="text-gray-600 font-semibold text-sm">常用菜单</Text>
|
||||
|
||||
<View className="p-1">
|
||||
<AListItem icon="LayoutGrid" text="类别设置" />
|
||||
<AListItem icon="ChartArea" text="图表分析" />
|
||||
</View>
|
||||
</View>
|
||||
</DrawerBody>
|
||||
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
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 {
|
||||
@@ -119,7 +118,7 @@ export default function StaticTransactions({
|
||||
</View>
|
||||
)}
|
||||
|
||||
renderItem={({ item, index }) => {
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<Menu
|
||||
renderer={renderers.Popover}
|
||||
@@ -182,40 +181,46 @@ export default function StaticTransactions({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuOption
|
||||
customStyles={{
|
||||
OptionTouchableComponent: TouchableHighlight,
|
||||
optionTouchable: {
|
||||
underlayColor: '#f0f0f0',
|
||||
activeOpacity: 0.7,
|
||||
},
|
||||
}}
|
||||
onSelect={() => {
|
||||
onUpdate?.(item)
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row items-center h-10 px-2">
|
||||
<LucideIcon icon="Pencil" />
|
||||
<Text className="ml-2 text-lg">更新</Text>
|
||||
</View>
|
||||
</MenuOption>
|
||||
<MenuOption
|
||||
customStyles={{
|
||||
OptionTouchableComponent: TouchableHighlight,
|
||||
optionTouchable: {
|
||||
underlayColor: '#f0f0f0',
|
||||
activeOpacity: 0.7,
|
||||
},
|
||||
}}
|
||||
onSelect={() => {
|
||||
onDelete?.(item.id)
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row items-center h-10 px-2">
|
||||
<LucideIcon icon="Trash" />
|
||||
<Text className="ml-2 text-lg">删除</Text>
|
||||
</View>
|
||||
</MenuOption>
|
||||
{
|
||||
userID === item.user_id && (
|
||||
<>
|
||||
<MenuOption
|
||||
customStyles={{
|
||||
OptionTouchableComponent: TouchableHighlight,
|
||||
optionTouchable: {
|
||||
underlayColor: '#f0f0f0',
|
||||
activeOpacity: 0.7,
|
||||
},
|
||||
}}
|
||||
onSelect={() => {
|
||||
onUpdate?.(item)
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row items-center h-10 px-2">
|
||||
<LucideIcon icon="Pencil" />
|
||||
<Text className="ml-2 text-lg">更新</Text>
|
||||
</View>
|
||||
</MenuOption>
|
||||
<MenuOption
|
||||
customStyles={{
|
||||
OptionTouchableComponent: TouchableHighlight,
|
||||
optionTouchable: {
|
||||
underlayColor: '#f0f0f0',
|
||||
activeOpacity: 0.7,
|
||||
},
|
||||
}}
|
||||
onSelect={() => {
|
||||
onDelete?.(item.id)
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row items-center h-10 px-2">
|
||||
<LucideIcon icon="Trash" />
|
||||
<Text className="ml-2 text-lg">删除</Text>
|
||||
</View>
|
||||
</MenuOption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<MenuOption
|
||||
customStyles={{
|
||||
OptionTouchableComponent: TouchableHighlight,
|
||||
|
||||
@@ -5,14 +5,16 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { ADatePicker, LucideIcon } from '@/components/common/ao'
|
||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||
import { AddIcon } from '@/components/ui/icon'
|
||||
import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
|
||||
import { queryMoneyRecordsApi } from '@/request/api/money/record'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||
import { getCurrentYearMonth } from '@/utils/time'
|
||||
import { toast } from '@/utils/toast'
|
||||
import ActionsMenu from './components/ActionsMenu'
|
||||
import TransactionsList from './components/TransactionsList'
|
||||
|
||||
export default function Money() {
|
||||
const appStore = useAppStore()
|
||||
const moneyRecordStore = useMoneyRecordStore()
|
||||
const router = useRouter()
|
||||
// 安全高度
|
||||
@@ -24,14 +26,16 @@ export default function Money() {
|
||||
record_time: getCurrentYearMonth(),
|
||||
})
|
||||
|
||||
// 操作菜单
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false)
|
||||
|
||||
function getMoneyRecords() {
|
||||
queryMoneyRecordsApi({
|
||||
record_time: searchParams.record_time,
|
||||
}).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)
|
||||
moneyRecordStore.setRecords(res.data.records, res.data.total_expense, res.data.total_income, res.data.total_balance, searchParams.record_time,
|
||||
)
|
||||
}
|
||||
else {
|
||||
toast.show({
|
||||
@@ -44,8 +48,10 @@ export default function Money() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!appStore.ready)
|
||||
return
|
||||
getMoneyRecords()
|
||||
}, [searchParams])
|
||||
}, [appStore.ready, searchParams])
|
||||
|
||||
return (
|
||||
|
||||
@@ -64,19 +70,30 @@ export default function Money() {
|
||||
onPress={() => setShowDatePicker(true)}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<Text className="font-bold text-lg">{searchParams.record_time}</Text>
|
||||
<Text className="font-semibold text-lg">{searchParams.record_time}</Text>
|
||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="flex flex-row items-center gap-2"
|
||||
onPress={() => {
|
||||
setShowActionsMenu(true)
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="Menu" size={24} clickable={false} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View className="mt-3 m-4 ">
|
||||
<View className="flex flex-row gap-4">
|
||||
<View
|
||||
className="px-3 py-3"
|
||||
>
|
||||
<View
|
||||
className="flex flex-row gap-4"
|
||||
style={{
|
||||
borderBottomLeftRadius: 16,
|
||||
borderBottomRightRadius: 18,
|
||||
}}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-gray-500 text-sm">收入</Text>
|
||||
<Text
|
||||
@@ -106,12 +123,12 @@ export default function Money() {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
0.00
|
||||
{moneyRecordStore.total_balance}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
{/* <View
|
||||
className="flex-row items-center gap-3 mt-2"
|
||||
style={{
|
||||
borderBottomLeftRadius: 8,
|
||||
@@ -125,7 +142,7 @@ export default function Money() {
|
||||
</Progress>
|
||||
</View>
|
||||
<Text className="text-xs text-blue-500">点击设置</Text>
|
||||
</View>
|
||||
</View> */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -177,6 +194,13 @@ export default function Money() {
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionsMenu
|
||||
isOpen={showActionsMenu}
|
||||
onClose={() => {
|
||||
setShowActionsMenu(false)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ErrorBoundaryProps } from 'expo-router'
|
||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator'
|
||||
import { useFonts } from 'expo-font'
|
||||
import { Stack, usePathname, useRouter } from 'expo-router'
|
||||
import { Stack, usePathname } from 'expo-router'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { useEffect } from 'react'
|
||||
@@ -11,11 +11,8 @@ 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 { useAppStore } from '@/store/app'
|
||||
import { useNavStore } from '@/store/navigation'
|
||||
import { useSpaceStore } from '@/store/space'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ToastProviderWrapper } from '@/utils/toast'
|
||||
import '@/styles/global.css'
|
||||
import 'react-native-reanimated'
|
||||
@@ -44,13 +41,10 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const spaceStore = useSpaceStore()
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const navStore = useNavStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const [fontsLoaded] = useFonts({
|
||||
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
|
||||
@@ -64,22 +58,9 @@ export default function RootLayout() {
|
||||
if (!fontsLoaded)
|
||||
return
|
||||
|
||||
// 登录跳转
|
||||
if (!userStore.isLoggedIn()) {
|
||||
navStore.navigateToAuth()
|
||||
}
|
||||
|
||||
// 如果登录的话,就初始化下
|
||||
moneyCategoryStore.init()
|
||||
|
||||
getUserSpaceApi({}).then((res) => {
|
||||
spaceStore.setUserSpace(res.data)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
appStore.init().finally(() => {
|
||||
SplashScreen.hideAsync()
|
||||
})
|
||||
|
||||
// 隐藏 splash
|
||||
SplashScreen.hideAsync()
|
||||
}, [fontsLoaded])
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +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 { useAppStore } from '@/store/app'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { toast } from '@/utils/toast'
|
||||
|
||||
@@ -21,7 +21,7 @@ interface LoginForm {
|
||||
export default function AuthLogin() {
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [form, setForm] = useState<LoginForm>({
|
||||
@@ -62,9 +62,10 @@ export default function AuthLogin() {
|
||||
refreshToken: res.data.refreshToken,
|
||||
rememberMe: form.rememberMe,
|
||||
})
|
||||
moneyCategoryStore.init()
|
||||
|
||||
router.replace('/user')
|
||||
appStore.init()
|
||||
|
||||
router.replace('/money')
|
||||
}
|
||||
else {
|
||||
toast.show({
|
||||
|
||||
@@ -5,11 +5,13 @@ import { AButton, ACell, ACellGroup, AModal } from '@/components/common/ao'
|
||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { toDateString, toDateTimeString } from '@/utils/time'
|
||||
|
||||
export default function MoneyRecordDetail() {
|
||||
const router = useRouter()
|
||||
const moneyRecordStore = useMoneyRecordStore()
|
||||
const userStore = useUserStore()
|
||||
const { id: detailId } = useLocalSearchParams<{ id: string }>()
|
||||
|
||||
const moneryRecord = moneyRecordStore.getMoneyRecordById(Number(detailId))
|
||||
@@ -36,31 +38,43 @@ export default function MoneyRecordDetail() {
|
||||
<ACell title="备注" rightText={moneryRecord.remark} />
|
||||
<ACell title="日期" rightText={toDateString(moneryRecord.record_time)} />
|
||||
<ACell title="操作日期" rightText={toDateTimeString(moneryRecord.update_time || moneryRecord.create_time)} />
|
||||
<ACell
|
||||
title="创建人"
|
||||
rightText={
|
||||
userStore.userInfo?.id === moneryRecord.user_id
|
||||
? '自己'
|
||||
: moneryRecord.username
|
||||
}
|
||||
/>
|
||||
</ACellGroup>
|
||||
|
||||
<View className="flex flex-row px-4 mt-4 gap-4">
|
||||
<AButton
|
||||
className="flex-1"
|
||||
action="negative"
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
setShowDeleteModal(true)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</AButton>
|
||||
<AButton
|
||||
className="flex-1"
|
||||
onPress={() => {
|
||||
router.navigate({
|
||||
pathname: '/money/record/editor',
|
||||
params: { id: detailId },
|
||||
})
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</AButton>
|
||||
</View>
|
||||
{userStore.userInfo?.id === moneryRecord.user_id && (
|
||||
<View className="flex flex-row px-4 mt-4 gap-4">
|
||||
|
||||
<AButton
|
||||
className="flex-1"
|
||||
action="negative"
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
setShowDeleteModal(true)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</AButton>
|
||||
<AButton
|
||||
className="flex-1"
|
||||
onPress={() => {
|
||||
router.navigate({
|
||||
pathname: '/money/record/editor',
|
||||
params: { id: detailId },
|
||||
})
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</AButton>
|
||||
</View>
|
||||
)}
|
||||
|
||||
</View>
|
||||
|
||||
</View>
|
||||
|
||||
@@ -61,10 +61,16 @@ export default function CategoryGrid({
|
||||
marginTop: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: isSelected ? '#BFDBFE' : '#F3F4F6',
|
||||
backgroundColor: isSelected ? '#333' : '#F3F4F6',
|
||||
}}
|
||||
>
|
||||
<MoneyCategoryIcon name={item.icon} size={24} />
|
||||
<MoneyCategoryIcon
|
||||
name={item.icon}
|
||||
size={24}
|
||||
color={
|
||||
isSelected ? '#fff' : '#000'
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
|
||||
@@ -93,6 +93,9 @@ export default function MoenyRecordEditor() {
|
||||
const result: MoneyRecordItem = {
|
||||
...moneyRecord,
|
||||
category_id: selectedCategory.id,
|
||||
category_type: selectedCategory.type,
|
||||
category_name: selectedCategory.name,
|
||||
category_icon: selectedCategory.icon,
|
||||
}
|
||||
|
||||
moneyRecordStore.updateMoneyRecord(result)
|
||||
@@ -114,6 +117,9 @@ export default function MoenyRecordEditor() {
|
||||
if (res.isSuccess) {
|
||||
const result: MoneyRecordItem = {
|
||||
...res.data,
|
||||
category_type: selectedCategory.type,
|
||||
category_name: selectedCategory.name,
|
||||
category_icon: selectedCategory.icon,
|
||||
}
|
||||
|
||||
moneyRecordStore.addMoneyRecord(result)
|
||||
|
||||
@@ -3,24 +3,20 @@ import { useState } from 'react'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import { AModal } from '@/components/common/ao'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
export default function Share() {
|
||||
const router = useRouter()
|
||||
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const [showExitModal, setShowExitModal] = useState(false)
|
||||
|
||||
function exitLogin() {
|
||||
setShowExitModal(false)
|
||||
|
||||
moneyCategoryStore.setCategories([])
|
||||
userStore.logout()
|
||||
appStore.reset()
|
||||
|
||||
router.navigate('/auth')
|
||||
router.replace('/auth')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -39,6 +35,6 @@ export default function Share() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { icons } from 'lucide-react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { Text, TouchableOpacity } from 'react-native'
|
||||
import LucideIcon from './LucideIcon'
|
||||
|
||||
export interface ListItemProps {
|
||||
icon: keyof typeof icons
|
||||
text: string
|
||||
href?: string
|
||||
onPress?: () => void
|
||||
showRightIcon?: boolean
|
||||
}
|
||||
|
||||
export default function ListItem({ icon, text, href, onPress, showRightIcon = false }: ListItemProps) {
|
||||
const router = useRouter()
|
||||
|
||||
const handlePress = () => {
|
||||
if (onPress) {
|
||||
onPress()
|
||||
return
|
||||
}
|
||||
|
||||
if (href) {
|
||||
router.navigate(href)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="flex flex-row items-center py-2"
|
||||
onPress={handlePress}
|
||||
>
|
||||
<LucideIcon icon={icon} size={18} strokeWidth={1.5} />
|
||||
|
||||
<Text className="text-base ml-3">{text}</Text>
|
||||
|
||||
{
|
||||
showRightIcon && (
|
||||
<LucideIcon
|
||||
icon="ChevronRight"
|
||||
className="ml-auto"
|
||||
color="#ccc"
|
||||
size={22}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Divider from './Divider'
|
||||
import Form, { FormRef } from './Form'
|
||||
import FormItem from './FormItem'
|
||||
import Input from './Input'
|
||||
import ListItem from './ListItem'
|
||||
import LucideIcon from './LucideIcon'
|
||||
import Modal from './Modal'
|
||||
import SegmentedTabs from './SegmentedTabs'
|
||||
@@ -22,6 +23,7 @@ export {
|
||||
FormItem as AFormItem,
|
||||
FormRef as AFormRef,
|
||||
Input as AInput,
|
||||
ListItem as AListItem,
|
||||
Modal as AModal,
|
||||
SegmentedTabs as ASegmentedTabs,
|
||||
Tab as ATab,
|
||||
|
||||
@@ -32,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,
|
||||
@@ -154,6 +154,7 @@ const alovaInstance = createAlova({
|
||||
const userSpace = useSpaceStore.getState().userSpace
|
||||
if (userSpace !== null) {
|
||||
const spaceID = userSpace.space_id
|
||||
|
||||
method.config.headers['x-space-id'] = spaceID
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { create } from 'zustand'
|
||||
import { getUserSpaceApi } from '@/request/api/space/spaceUser'
|
||||
import { useMoneyCategoryStore } from './moneyCategory'
|
||||
import { useNavStore } from './navigation'
|
||||
import { useSpaceStore } from './space'
|
||||
import { useUserStore } from './user'
|
||||
|
||||
interface AppState {
|
||||
ready: boolean
|
||||
setReady: (v: boolean) => void
|
||||
init: () => Promise<void>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => ({
|
||||
ready: false,
|
||||
|
||||
setReady: (v: boolean) => set({ ready: v }),
|
||||
|
||||
init: async () => {
|
||||
// 如果已经初始化过就不重复执行(除非你手动 setReady(false) 再 init)
|
||||
if (get().ready)
|
||||
return
|
||||
|
||||
const userStore = useUserStore.getState()
|
||||
const navStore = useNavStore.getState()
|
||||
const moneyCategoryStore = useMoneyCategoryStore.getState()
|
||||
const spaceStore = useSpaceStore.getState()
|
||||
|
||||
// 未登录:跳转到登录页,但认为初始化已完成(允许 UI 继续渲染)
|
||||
if (!userStore.isLoggedIn()) {
|
||||
navStore.navigateToAuth()
|
||||
set({ ready: true })
|
||||
return
|
||||
}
|
||||
|
||||
// 登录状态:执行完整初始化流程
|
||||
try {
|
||||
// 分类初始化
|
||||
await moneyCategoryStore.init()
|
||||
|
||||
// 获取空间信息
|
||||
const res = await getUserSpaceApi({})
|
||||
spaceStore.setUserSpace(res.data)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('app init error:', err)
|
||||
}
|
||||
|
||||
// 初始化完成
|
||||
set({ ready: true })
|
||||
},
|
||||
reset: () => {
|
||||
set({ ready: false })
|
||||
|
||||
useMoneyCategoryStore.getState().clear()
|
||||
useUserStore.getState().clear()
|
||||
useSpaceStore.getState().clear()
|
||||
},
|
||||
}))
|
||||
@@ -22,17 +22,19 @@ interface MoneyCategoryStore {
|
||||
|
||||
sysCategories: MoneyCategoryItem[]
|
||||
setSysCategories: (categories: MoneyCategoryItem[]) => void
|
||||
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
categories: [],
|
||||
sysCategories:[],
|
||||
sysCategories: [],
|
||||
|
||||
setCategories: categories => set({ categories }),
|
||||
setSysCategories: categories => set({ categories }),
|
||||
|
||||
|
||||
addCategory: category =>
|
||||
set({ categories: [...get().categories, category] }),
|
||||
|
||||
@@ -56,6 +58,10 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
set({ categories: [] })
|
||||
},
|
||||
|
||||
getExpenseCategories: () => (get().categories || []).filter(c => c.type === 0),
|
||||
getIncomeCategories: () => (get().categories || []).filter(c => c.type === 1),
|
||||
|
||||
@@ -132,7 +138,6 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
}))
|
||||
await updateMoneyCategorySortApi({ data: payload })
|
||||
},
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'money-category-storage',
|
||||
|
||||
@@ -11,8 +11,9 @@ interface MoneyRecordStore {
|
||||
|
||||
total_income: string
|
||||
total_expense: string
|
||||
total_balance: string
|
||||
|
||||
setRecords: (records: MoneyRecordGroup[], total_expense: string, total_income: string, month: string) => void
|
||||
setRecords: (records: MoneyRecordGroup[], total_expense: string, total_income: string, total_balance: string, month: string) => void
|
||||
setCurrentMonth: (month: string) => void
|
||||
|
||||
addMoneyRecord: (data: MoneyRecordItem) => void
|
||||
@@ -28,9 +29,10 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
|
||||
total_income: '0.00',
|
||||
total_expense: '0.00',
|
||||
total_balance: '0.00',
|
||||
|
||||
setRecords: (records, total_expense, total_income, month) => {
|
||||
set({ records, currentMonth: month, total_income, total_expense })
|
||||
setRecords: (records, total_expense, total_income, total_balance, month) => {
|
||||
set({ records, currentMonth: month, total_income, total_expense, total_balance })
|
||||
},
|
||||
|
||||
setCurrentMonth: month => set({ currentMonth: month }),
|
||||
@@ -66,11 +68,15 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
else records.splice(insertIndex, 0, newGroup)
|
||||
}
|
||||
|
||||
const totalExpense = category?.type === 0 ? add(prev.total_expense, amount) : prev.total_expense
|
||||
const totalIncome = category?.type === 1 ? add(prev.total_income, amount) : prev.total_income
|
||||
const totalBalance = sub(totalIncome, totalExpense)
|
||||
return {
|
||||
...prev,
|
||||
records,
|
||||
total_expense: category?.type === 0 ? add(prev.total_expense, amount) : prev.total_expense,
|
||||
total_income: category?.type === 1 ? add(prev.total_income, amount) : prev.total_income,
|
||||
total_expense: totalExpense,
|
||||
total_income: totalIncome,
|
||||
total_balance: totalBalance,
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -136,7 +142,9 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
if (updateRecordCategory?.type === 1)
|
||||
total_income = add(total_income, newAmount)
|
||||
|
||||
return { ...prev, records, total_expense, total_income }
|
||||
const total_balance = sub(total_income, total_expense)
|
||||
|
||||
return { ...prev, records, total_expense, total_income, total_balance }
|
||||
})
|
||||
},
|
||||
|
||||
@@ -161,7 +169,10 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
})
|
||||
|
||||
const newRecords = records.filter(g => g.records.length > 0)
|
||||
return { ...prev, records: newRecords, total_expense, total_income }
|
||||
|
||||
const total_balance = sub(total_income, total_expense)
|
||||
|
||||
return { ...prev, records: newRecords, total_expense, total_income, total_balance }
|
||||
})
|
||||
|
||||
deleteMoneyRecordApi({ id }).catch(console.error)
|
||||
|
||||
@@ -3,9 +3,15 @@ import { create } from 'zustand'
|
||||
interface SpaceState {
|
||||
userSpace: UserSpace | null
|
||||
setUserSpace: (space: UserSpace | null) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
export const useSpaceStore = create<SpaceState>(set => ({
|
||||
userSpace: null,
|
||||
setUserSpace: space => set({ userSpace: space }),
|
||||
clear: () => {
|
||||
set({
|
||||
userSpace: null,
|
||||
})
|
||||
},
|
||||
}))
|
||||
@@ -6,7 +6,7 @@ import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
interface UserState {
|
||||
userInfo: UserInfo | null
|
||||
setUserInfo: (userInfo: UserInfo | null) => void
|
||||
logout: () => void
|
||||
clear: () => void
|
||||
isLoggedIn: () => boolean
|
||||
|
||||
userSpace: UserSpace | null
|
||||
@@ -22,7 +22,7 @@ const useUserStore = create<UserState>()(
|
||||
setUserInfo: userInfo => set({ userInfo }),
|
||||
setUserSpace: userSpace => set({ userSpace }),
|
||||
|
||||
logout: () => {
|
||||
clear: () => {
|
||||
set({ userInfo: null, userSpace: null })
|
||||
},
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface QueryMoneyRecordsResult {
|
||||
records: MoneyRecordGroup[]
|
||||
total_income: string
|
||||
total_expense: string
|
||||
total_balance: string
|
||||
}
|
||||
|
||||
// 新增请求参数
|
||||
|
||||
@@ -1,180 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>字段配置示例</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial;
|
||||
padding: 20px;
|
||||
}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Drawer Menu</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
/* Drawer 背景遮罩 */
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.35);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
}
|
||||
.drawer-overlay.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.config-row td {
|
||||
background: #fafafa;
|
||||
padding: 15px;
|
||||
}
|
||||
/* Drawer 主体 */
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -280px; /* 初始隐藏 */
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
|
||||
transition: 0.3s;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.drawer.show {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.config-group {
|
||||
flex: 1;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
}
|
||||
/* Drawer 顶部标题 */
|
||||
.drawer-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-group h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* 分组标题 */
|
||||
.group-title {
|
||||
margin: 20px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/* 每个功能按钮 */
|
||||
.drawer-item {
|
||||
padding: 12px 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8f8f8;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.config-item label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.drawer-item:hover {
|
||||
background: #ececec;
|
||||
}
|
||||
|
||||
.config-item input[type="text"],
|
||||
.config-item input[type="number"],
|
||||
.config-item textarea {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
margin-top: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
/* 打开 Drawer 按钮(演示用) */
|
||||
.open-btn {
|
||||
margin: 30px;
|
||||
padding: 10px 20px;
|
||||
background: #0080ff;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="open-btn" onclick="openDrawer()">打开菜单</div>
|
||||
|
||||
<h2>字段配置示例</h2>
|
||||
<!-- 遮罩 -->
|
||||
<div class="drawer-overlay" id="drawerOverlay" onclick="closeDrawer()"></div>
|
||||
|
||||
<table id="fieldTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>字段名</th>
|
||||
<th>类型</th>
|
||||
<th>必填</th>
|
||||
<th style="width: 80px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fieldTableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Drawer -->
|
||||
<div class="drawer" id="drawer">
|
||||
<div class="drawer-title">功能菜单</div>
|
||||
|
||||
<script>
|
||||
const fields = [
|
||||
{ id: 1, name: "username", type: "string", required: true, default: "", repeat: false, maxLen: 20, desc: "" },
|
||||
{ id: 2, name: "age", type: "number", required: false, default: 0, repeat: false, maxLen: 3, desc: "" },
|
||||
];
|
||||
<div class="group-title">账单操作</div>
|
||||
<div class="drawer-item">新增账单</div>
|
||||
<div class="drawer-item">批量管理</div>
|
||||
<div class="drawer-item">分类管理</div>
|
||||
|
||||
let openConfigRowId = null;
|
||||
<div class="group-title">数据统计</div>
|
||||
<div class="drawer-item">月度报表</div>
|
||||
<div class="drawer-item">年度报表</div>
|
||||
<div class="drawer-item">图表分析</div>
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById("fieldTableBody");
|
||||
tbody.innerHTML = "";
|
||||
<div class="group-title">设置</div>
|
||||
<div class="drawer-item">预算管理</div>
|
||||
<div class="drawer-item">数据导出</div>
|
||||
<div class="drawer-item">云同步</div>
|
||||
|
||||
fields.forEach(field => {
|
||||
// 主行
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${field.name}</td>
|
||||
<td>${field.type}</td>
|
||||
<td>${field.required ? "是" : "否"}</td>
|
||||
<td><button onclick="toggleConfig(${field.id})">配置</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
</div>
|
||||
|
||||
// 配置行
|
||||
if (openConfigRowId === field.id) {
|
||||
const cfgRow = document.createElement("tr");
|
||||
cfgRow.className = "config-row";
|
||||
cfgRow.innerHTML = `
|
||||
<td colspan="4">
|
||||
${renderConfigPanel(field)}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(cfgRow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleConfig(id) {
|
||||
openConfigRowId = openConfigRowId === id ? null : id;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function renderConfigPanel(field) {
|
||||
return `
|
||||
<div class="config-panel">
|
||||
|
||||
<div class="config-group">
|
||||
<h4>基础配置</h4>
|
||||
<div class="config-item">
|
||||
<label>字段名</label>
|
||||
<input type="text" value="${field.name}" oninput="updateField(${field.id}, 'name', this.value)">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>默认值</label>
|
||||
<input type="text" value="${field.default}" oninput="updateField(${field.id}, 'default', this.value)">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label><input type="checkbox" ${field.repeat ? "checked" : ""} onchange="updateField(${field.id}, 'repeat', this.checked)"> 可重复</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-group">
|
||||
<h4>校验配置</h4>
|
||||
<div class="config-item">
|
||||
<label>必填</label>
|
||||
<input type="checkbox" ${field.required ? "checked" : ""} onchange="updateField(${field.id}, 'required', this.checked)">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>最大长度</label>
|
||||
<input type="number" value="${field.maxLen}" oninput="updateField(${field.id}, 'maxLen', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-group">
|
||||
<h4>展示配置</h4>
|
||||
<div class="config-item">
|
||||
<label>说明</label>
|
||||
<textarea rows="4" oninput="updateField(${field.id}, 'desc', this.value)">${field.desc}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateField(id, key, value) {
|
||||
const field = fields.find(f => f.id === id);
|
||||
field[key] = value;
|
||||
}
|
||||
|
||||
renderTable();
|
||||
</script>
|
||||
<script>
|
||||
function openDrawer() {
|
||||
document.getElementById("drawer").classList.add("show");
|
||||
document.getElementById("drawerOverlay").classList.add("show");
|
||||
}
|
||||
function closeDrawer() {
|
||||
document.getElementById("drawer").classList.remove("show");
|
||||
document.getElementById("drawerOverlay").classList.remove("show");
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
Reference in new issue
Block a user