This commit is contained in:
oneao committed 2025-11-18 17:28:34 +08:00
1 parent eaba6cf17b
commit 1445ed3b6c
19 files changed
+613 -401

No files matched your search

@@ -14,9 +14,9 @@ export default function TabsLayout() {
icon: 'CircleDollarSign',
},
{
name: 'user',
href: '/user',
icon: 'CircleCheckBig',
name: 'love',
href: '/love',
icon: 'Heart',
},
{
name: 'app',
@@ -35,9 +35,9 @@ export default function TabsLayout() {
icon: 'MessageCircle',
},
{
name: 'love',
href: '/love',
icon: 'Heart',
name: 'user',
href: '/user',
icon: 'CircleUserRound',
},
]
@@ -1,68 +0,0 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'
const Locales = ['zh-cn', 'en', 'de', 'es']
interface Props {
locale: string
setLocale: (locale: string) => void
mainColor?: string
activeTextColor?: string
}
export default function LocaleSelector({
locale,
setLocale,
mainColor,
activeTextColor,
}: Props) {
return (
<View style={styles.localeContainer}>
<Text style={{ marginRight: 8 }}>Locale:</Text>
{Locales.map((item, index) => (
<Pressable
key={index}
style={[
styles.localeButton,
item === locale && {
backgroundColor: mainColor,
},
]}
onPress={() => setLocale(item)}
accessibilityRole="button"
accessibilityLabel={item.toUpperCase()}
>
<Text
style={[
styles.localeButtonText,
item === locale && {
fontWeight: 'bold',
color: activeTextColor,
},
]}
>
{item.split('-')[0]?.toUpperCase()}
</Text>
</Pressable>
))}
</View>
)
}
const styles = StyleSheet.create({
localeContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
localeButton: {
alignItems: 'center',
justifyContent: 'center',
width: 36,
height: 36,
borderRadius: 36,
margin: 2,
},
localeButtonText: {
fontSize: 15,
},
})
@@ -1,54 +0,0 @@
import { Pressable, StyleSheet, View } from 'react-native'
export interface ITheme {
mainColor: string
activeTextColor: string
}
interface Props {
themes: ITheme[]
setTheme: (theme: ITheme) => void
}
export default function ThemeSelector({ themes = [], setTheme }: Props) {
return (
<View style={styles.themeContainer}>
{themes.map((item, index) => (
<Pressable
key={index}
style={[
styles.themeButton,
{
borderColor: item.activeTextColor,
backgroundColor: item.mainColor,
},
]}
onPress={() => setTheme(item)}
accessibilityRole="button"
accessibilityLabel="Set Active Theme"
/>
))}
</View>
)
}
const styles = StyleSheet.create({
themeContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
marginBottom: 10,
width: 330,
},
themeButton: {
borderWidth: 4,
width: 32,
height: 32,
borderRadius: 32,
margin: 5,
shadowRadius: 20,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowOffset: { width: 0, height: 0 },
},
})
@@ -1,6 +1,6 @@
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
import React, { useCallback, useState } from 'react'
import { Pressable, SectionList, Text, View } from 'react-native'
import React, { useState } from 'react'
import { SectionList, Text, TouchableHighlight, View } from 'react-native'
import {
Menu,
MenuOption,
@@ -50,7 +50,7 @@ export default function StaticTransactions({
stickySectionHeadersEnabled={true}
showsVerticalScrollIndicator={false}
renderSectionHeader={({ section }) => (
<View className="bg-white py-2 px-4 border-b border-gray-100 mb-2">
<View className="bg-white py-2 px-4 border-b border-gray-50">
<View className="flex-row items-center justify-between">
<Text className="text-gray-700 font-semibold text-base">
{section.title}
@@ -80,7 +80,7 @@ export default function StaticTransactions({
</View>
)}
renderItem={({ item }) => {
renderItem={({ item, index }) => {
return (
<Menu
renderer={renderers.Popover}
@@ -89,36 +89,47 @@ export default function StaticTransactions({
preferredPlacement: 'bottom',
anchorStyle: { backgroundColor: 'transparent' },
}}
onSelect={() => setSelectedId(null)}
onBackdropPress={() => setSelectedId(null)}
onOpen={() => setSelectedId(item.id)}
onClose={() => setSelectedId(null)}
>
<MenuTrigger>
<MenuTrigger triggerOnLongPress>
<View
className="flex-row items-center gap-4 py-2 px-5"
style={{
backgroundColor: item.id === selectedId ? '#f2f2f2' : 'transparent',
}}
>
<View className="w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
<MoneyCategoryIcon name={item.category_icon} size={22} color="#000" />
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
<MoneyCategoryIcon name={item.category_icon} size={20} color="#000" />
</View>
<View className="flex-1">
<Text className="text-gray-800 font-medium text-base">{item.category_name}</Text>
{item.remark && <Text className="text-gray-400 text-sm">{item.remark}</Text>}
<Text className="text-gray-800 text-base">
{item.remark ? item.remark : item.category_name}
</Text>
{
index === 1 && (
<Text className="text-gray-500 text-xs italic">
操作人:张三
</Text>
)
}
</View>
<View className="flex-row flex">
<Text className="text-sm text-gray-500">
<View>
<Text
className="text-sm text-gray-600 text-right"
>
{item.category_type === 1 ? '+' : '-'}
{' '}
{item.amount}
</Text>
</View>
<Badge size="sm" variant="solid" action="muted">
<BadgeText>宝贝</BadgeText>
</Badge>
</View>
</MenuTrigger>
<MenuOptions
customStyles={{
optionsContainer: {
@@ -129,25 +140,36 @@ export default function StaticTransactions({
>
<MenuOption
customStyles={{
OptionTouchableComponent: TouchableHighlight,
optionTouchable: {
underlayColor: '#f0f0f0', // 按压时的背景色
activeOpacity: 0.7, // (可选)按压时的透明度
},
// 如果需要,也可以设置默认状态下的背景色
optionWrapper: {
backgroundColor: 'transparent', // 默认背景色
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 onSelect={() => alert(`Clicked Option B fo`)}>
<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>
<Text className="ml-2 text-lg">删除</Text>
</View>
</MenuOption>
</MenuOptions>
@@ -1,6 +1,6 @@
import type { MoneyRecordGroup, MoneyRecordItem, QueryMoneyRecordsResult } from '@/types/money/record'
import React, { useEffect, useState } from 'react'
import { Text, TouchableOpacity, View } from 'react-native'
import { Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { ADatePicker, LucideIcon } from '@/components/common/ao'
import { Fab, FabIcon } from '@/components/ui/fab'
@@ -277,13 +277,17 @@ export default function Money() {
}
return (
<View
className="flex-1"
style={{
paddingTop: insets.top,
}}
>
<View>
<View
style={{
paddingTop: insets.top,
backgroundColor: '#fff',
elevation: 3,
}}
>
<View className="flex flex-row justify-between items-center px-3">
<TouchableOpacity
onPress={() => setShowDatePicker(true)}
@@ -300,45 +304,51 @@ export default function Money() {
</TouchableOpacity>
</View>
<View className="mt-3 border border-gray-200 rounded-lg m-4 p-4 shadow-sm bg-white">
<View
style={{
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
}}
>
<View className="mb-4">
<Text className="text-gray-500 text-sm">
余额
{/* <Text className="text-red-500">↓20%</Text> */}
<View className="mt-3 m-4 ">
<View className="flex flex-row gap-4">
<View className="flex-1">
<Text className="text-gray-500 text-sm">余额</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
0.00
</Text>
<Text className="font-bold text-2xl mt-0.5">22,313.00</Text>
</View>
<View className="flex-row flex gap-8">
<View>
<Text className="text-gray-500 text-sm">
收入
{/* <Text className="text-green-500">↑20%</Text> */}
</Text>
<Text className="text-lg font-semibold mt-0.5">{moneyRecords.total_income}</Text>
</View>
<View className="flex-1">
<Text className="text-gray-500 text-sm">收入</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
0.00
</Text>
</View>
<View>
<Text className="text-gray-500 text-sm">
支出
{/* <Text className="text-green-500">↑20%</Text> */}
</Text>
<Text className="text-lg font-semibold mt-0.5">{moneyRecords.total_expense}</Text>
</View>
{/*
<View>
<Text className="text-gray-500 text-sm">
结余
<Text className="text-green-500">↑20%</Text>
</Text>
<Text className="text-lg font-semibold mt-0.5">11.0</Text>
</View> */}
<View className="flex-1">
<Text className="text-gray-500 text-sm">支出</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
0.00
</Text>
</View>
<View className="flex-1">
<Text className="text-gray-500 text-sm">结余</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
0.00
</Text>
</View>
</View>
@@ -1,88 +0,0 @@
import { router } from 'expo-router'
import { StyleSheet, Text, View } from 'react-native'
import { AButton, AInput } from '@/components/common/ao'
import { LucideIcon } from '@/components/common/ao/index'
import { useUserStore } from '@/store/user'
export default function User() {
const userInfo = useUserStore(state => state.userInfo)
if (!userInfo) {
return (
<View style={styles.container}>
<Text style={styles.empty}>未登录</Text>
</View>
)
}
const genderText = userInfo.gender === 1 ? '男' : userInfo.gender === 2 ? '女' : '未设置'
return (
<View style={styles.container}>
<Text style={styles.title}>个人信息</Text>
<View style={styles.infoBox}>
<Text style={styles.item}>
账号:
{userInfo.account}
</Text>
<Text style={styles.item}>
用户名:
{userInfo.username}
</Text>
<Text style={styles.item}>
ID:
{userInfo.id}
</Text>
<Text style={styles.item}>
序号:
{userInfo.seqNo}
</Text>
<Text style={styles.item}>
性别:
{genderText}
</Text>
{userInfo.rememberMe && <Text style={styles.item}>已开启记住我功能</Text>}
</View>
<AButton onPress={() => {
router.replace('/auth')
}}
>
跳转到登陆页面
</AButton>
<AInput value="2" />
<LucideIcon icon="User" size={32} strokeWidth={1} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
},
infoBox: {
backgroundColor: '#f8f8f8',
borderRadius: 12,
padding: 16,
},
item: {
fontSize: 16,
marginBottom: 8,
color: '#333',
},
empty: {
textAlign: 'center',
marginTop: 100,
fontSize: 18,
color: '#888',
},
})
@@ -0,0 +1,45 @@
import type { icons } from 'lucide-react-native'
import { useRouter } from 'expo-router'
import { Text, TouchableOpacity } from 'react-native'
import { LucideIcon } from '@/components/common/ao/index'
export interface ListItemProps {
icon: keyof typeof icons
text: string
href?: string
onPress?: () => void
}
export default function ListItem({ icon, text, href, onPress }: ListItemProps) {
const router = useRouter()
const handlePress = () => {
if (onPress) {
onPress()
return
}
if (href) {
router.navigate(href)
}
}
return (
<TouchableOpacity
className="flex flex-row items-center py-3"
onPress={handlePress}
>
<LucideIcon icon={icon} size={24} strokeWidth={1.5} />
<Text className="text-base ml-3">{text}</Text>
<LucideIcon
icon="ChevronRight"
className="ml-auto"
color="#ccc"
size={22}
strokeWidth={1.5}
/>
</TouchableOpacity>
)
}
@@ -0,0 +1,30 @@
import { Text, TouchableOpacity, 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 ListItem from './components/ListItem'
export default function User() {
return (
<SafeAreaView className="flex-1 p-4">
<View className="flex flex-row items-center ">
<Avatar size="md" className="border-0">
<AvatarImage
className="bg-white"
source={require('@/static/images/logo.png')}
/>
</Avatar>
<Text className="text-xl font-semibold ml-3">张三</Text>
<LucideIcon icon="ChevronRight" size={26} className="ml-auto" color="#aaa" strokeWidth={1.5} />
</View>
<View className="mt-8 flex flex-col">
<ListItem icon="Earth" text="共享空间" href="/system/share" />
<ListItem icon="Settings" text="设置"href="/system/settings" />
</View>
</SafeAreaView>
)
}
@@ -0,0 +1,9 @@
import { Text, View } from 'react-native'
export default function Settings() {
return (
<View>
<Text>Settings</Text>
</View>
)
}
@@ -0,0 +1,17 @@
import { Stack } from 'expo-router'
import { Text, View } from 'react-native'
export default function Share() {
return (
<>
<Stack.Screen options={{
headerShown: false,
}}
/>
<View>
<Text>Share</Text>
</View>
</>
)
}
@@ -0,0 +1,17 @@
import { Stack } from 'expo-router'
import { SafeAreaView } from 'react-native-safe-area-context'
export default function CustomHeader() {
return (
<>
<Stack.Screen options={{
headerShown: false,
}}
/>
<SafeAreaView className='flex-1'>
</SafeAreaView>
</>
)
}
@@ -1,4 +1,3 @@
import { Ionicons } from '@expo/vector-icons'
import { usePathname, useRouter } from 'expo-router'
import { useRef } from 'react'
import { Image, Pressable, Text, View } from 'react-native'
@@ -55,7 +54,7 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
onPress={canGoBack ? (currentStep.onBack ?? (() => router.back())) : undefined}
style={{ opacity: canGoBack ? 1 : 0 }}
>
<Ionicons name="arrow-back" size={22} />
{/* <Ionicons name="arrow-back" size={22} /> */}
</Pressable>
<Text className="flex-1 text-center text-lg font-semibold">
@@ -0,0 +1,185 @@
'use client';
import React from 'react';
import { createAvatar } from '@gluestack-ui/core/avatar/creator';
import { View, Text, Image, Platform } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/utils/nativewind-utils';
const SCOPE = 'AVATAR';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
const UIAvatar = createAvatar({
Root: withStyleContext(View, SCOPE),
Badge: View,
Group: View,
Image: Image,
FallbackText: Text,
});
const avatarStyle = tva({
base: 'rounded-full justify-center items-center relative bg-primary-600 group-[.avatar-group]/avatar-group:-ml-2.5',
variants: {
size: {
'xs': 'w-6 h-6',
'sm': 'w-8 h-8',
'md': 'w-12 h-12',
'lg': 'w-16 h-16',
'xl': 'w-24 h-24',
'2xl': 'w-32 h-32',
},
},
});
const avatarFallbackTextStyle = tva({
base: 'text-typography-0 font-semibold overflow-hidden text-transform:uppercase web:cursor-default',
parentVariants: {
size: {
'xs': 'text-2xs',
'sm': 'text-xs',
'md': 'text-base',
'lg': 'text-xl',
'xl': 'text-3xl',
'2xl': 'text-5xl',
},
},
});
const avatarGroupStyle = tva({
base: 'group/avatar-group flex-row-reverse relative avatar-group',
});
const avatarBadgeStyle = tva({
base: 'w-5 h-5 bg-success-500 rounded-full absolute right-0 bottom-0 border-background-0 border-2',
parentVariants: {
size: {
'xs': 'w-2 h-2',
'sm': 'w-2 h-2',
'md': 'w-3 h-3',
'lg': 'w-4 h-4',
'xl': 'w-6 h-6',
'2xl': 'w-8 h-8',
},
},
});
const avatarImageStyle = tva({
base: 'h-full w-full rounded-full absolute',
});
type IAvatarProps = Omit<
React.ComponentPropsWithoutRef<typeof UIAvatar>,
'context'
> &
VariantProps<typeof avatarStyle>;
const Avatar = React.forwardRef<
React.ComponentRef<typeof UIAvatar>,
IAvatarProps
>(function Avatar({ className, size = 'md', ...props }, ref) {
return (
<UIAvatar
ref={ref}
{...props}
className={avatarStyle({ size, class: className })}
context={{ size }}
/>
);
});
type IAvatarBadgeProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Badge> &
VariantProps<typeof avatarBadgeStyle>;
const AvatarBadge = React.forwardRef<
React.ComponentRef<typeof UIAvatar.Badge>,
IAvatarBadgeProps
>(function AvatarBadge({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIAvatar.Badge
ref={ref}
{...props}
className={avatarBadgeStyle({
parentVariants: {
size: parentSize,
},
size,
class: className,
})}
/>
);
});
type IAvatarFallbackTextProps = React.ComponentPropsWithoutRef<
typeof UIAvatar.FallbackText
> &
VariantProps<typeof avatarFallbackTextStyle>;
const AvatarFallbackText = React.forwardRef<
React.ComponentRef<typeof UIAvatar.FallbackText>,
IAvatarFallbackTextProps
>(function AvatarFallbackText({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIAvatar.FallbackText
ref={ref}
{...props}
className={avatarFallbackTextStyle({
parentVariants: {
size: parentSize,
},
size,
class: className,
})}
/>
);
});
type IAvatarImageProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Image> &
VariantProps<typeof avatarImageStyle>;
const AvatarImage = React.forwardRef<
React.ComponentRef<typeof UIAvatar.Image>,
IAvatarImageProps
>(function AvatarImage({ className, ...props }, ref) {
return (
<UIAvatar.Image
ref={ref}
{...props}
className={avatarImageStyle({
class: className,
})}
// @ts-expect-error : This is a workaround to fix the issue with the image style on web.
style={
Platform.OS === 'web'
? { height: 'revert-layer', width: 'revert-layer' }
: undefined
}
/>
);
});
type IAvatarGroupProps = React.ComponentPropsWithoutRef<typeof UIAvatar.Group> &
VariantProps<typeof avatarGroupStyle>;
const AvatarGroup = React.forwardRef<
React.ComponentRef<typeof UIAvatar.Group>,
IAvatarGroupProps
>(function AvatarGroup({ className, ...props }, ref) {
return (
<UIAvatar.Group
ref={ref}
{...props}
className={avatarGroupStyle({
class: className,
})}
/>
);
});
export { Avatar, AvatarBadge, AvatarFallbackText, AvatarImage, AvatarGroup };
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
baseURL: 'http://192.168.5.6:8085/app',
// baseURL: 'http://192.168.1.136:8085/app',
// baseURL: 'http://192.168.5.6:8085/app',
baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="430.493" viewBox="0 0 800 430.493" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" artist="Katerina Limpitsouni" source="https://undraw.co/"><g transform="translate(-368.866 -254.661)"><ellipse cx="149.199" cy="26.107" rx="149.199" ry="26.107" transform="translate(368.866 481.453)" fill="#090814"/><ellipse cx="191.906" cy="26.107" rx="191.906" ry="26.107" transform="translate(785.054 632.939)" fill="#090814"/><path d="M527.508,703.017a139.876,139.876,0,0,1-34.251-3.807l1.007-3.942c61.693,15.766,176.375-20.484,188.785-59.673l3.879,1.228C676.145,670.877,591.731,703.017,527.508,703.017Z" transform="translate(229.585 -61.587)" fill="#2f2e41"/><path d="M422.29,529.663c-14.453-3.693-21.922-7.965-24.218-13.849-2.856-7.316,2.5-15.561,9.919-26.972,11.315-17.409,26.811-41.252,24.3-77.646-1.418-20.568-8.78-35.7-21.879-44.978-26.656-18.878-68.707-7.567-69.129-7.451l-1.085-3.921c1.8-.5,44.3-11.949,72.551,8.041,14.153,10.017,22.094,26.176,23.6,48.028,2.6,37.751-13.977,63.262-24.949,80.143-6.756,10.394-11.637,17.9-9.54,23.275,1.748,4.477,8.559,8.1,21.436,11.386Z" transform="translate(278.852 29.977)" fill="#2f2e41"/><path d="M816.039,661.123,795.384,696.05s-26.3,13.971-7.807,14.382,105.6,0,105.6,0,16.847,0-9.862-14.793l-19.389-37.478Z" transform="translate(136.968 -68.851)" fill="#e6e6e6"/><rect width="348.044" height="242.439" rx="18.046" transform="translate(801.887 355.38)" fill="#e6e6e6"/><path d="M2.712,0h319.2a2.712,2.712,0,0,1,2.712,2.712V193.294a2.712,2.712,0,0,1-2.712,2.712H2.712A2.712,2.712,0,0,1,0,193.294V2.712A2.712,2.712,0,0,1,2.712,0Z" transform="translate(813.598 368.323)" fill="#6c63ff"/><circle cx="2.055" cy="2.055" r="2.055" transform="translate(973.854 360.516)" fill="#f2f2f2"/><path d="M957.868,621.322v21.252a12.236,12.236,0,0,1-12.239,12.237H622.061a12.236,12.236,0,0,1-12.237-12.239V621.322Z" transform="translate(192.063 -56.993)" fill="#e6e6e6"/><path d="M864.04,660.444v4.109H642.968v-3.288l.3-.822,5.449-14.793h210.8Z" transform="translate(202.681 3.326)" fill="#f2f2f2"/><path d="M1096.367,755.317c-.4,1.722-1.923,3.538-5.358,5.256-12.327,6.164-37.393-1.644-37.393-1.644s-19.313-3.287-19.313-11.917a15.428,15.428,0,0,1,1.693-1.007c5.183-2.742,22.367-9.508,52.837.286a12.709,12.709,0,0,1,5.792,3.8C1095.859,751.534,1096.828,753.367,1096.367,755.317Z" transform="translate(55.429 -95.57)" fill="#f2f2f2"/><path d="M1097.171,755.317c-15.089,5.782-28.538,6.213-42.34-3.374-6.961-4.832-13.285-6.028-18.031-5.938,5.183-2.742,22.367-9.508,52.837.286a12.708,12.708,0,0,1,5.792,3.8C1096.663,751.534,1097.631,753.367,1097.171,755.317Z" transform="translate(54.626 -95.57)" fill="#e6e6e6"/><path d="M864.04,667.466v4.109H642.968v-3.288l.3-.822Z" transform="translate(202.681 -3.696)" opacity="0.1"/><path d="M794.285,488.635H592.452a6.086,6.086,0,0,1-6.079-6.079V414.467a6.086,6.086,0,0,1,6.079-6.079H794.285a6.086,6.086,0,0,1,6.079,6.079v68.088A6.086,6.086,0,0,1,794.285,488.635Z" transform="translate(-181.314 -153.727)" fill="#6c63ff"/><rect width="53.498" height="6.079" transform="translate(436.671 272.899)" fill="#f2f2f2"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(557.041 272.899)" fill="#f2f2f2"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(569.2 272.899)" fill="#f2f2f2"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(581.358 272.899)" fill="#f2f2f2"/><path d="M794.285,601.689H592.452a6.087,6.087,0,0,1-6.079-6.079V527.521a6.086,6.086,0,0,1,6.079-6.079H794.285a6.086,6.086,0,0,1,6.079,6.079v68.088A6.086,6.086,0,0,1,794.285,601.689Z" transform="translate(-181.314 -180.455)" fill="#e6e6e6"/><rect width="53.498" height="6.079" transform="translate(436.671 359.225)" fill="#6c63ff"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(557.041 359.225)" fill="#6c63ff"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(569.2 359.225)" fill="#6c63ff"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(581.358 359.225)" fill="#6c63ff"/><path d="M794.285,714.743H592.452a6.087,6.087,0,0,1-6.079-6.079V640.575a6.086,6.086,0,0,1,6.079-6.079H794.285a6.086,6.086,0,0,1,6.079,6.079v68.089A6.086,6.086,0,0,1,794.285,714.743Z" transform="translate(-181.314 -207.183)" fill="#e6e6e6"/><rect width="53.498" height="6.079" transform="translate(436.671 445.552)" fill="#6c63ff"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(557.041 445.552)" fill="#6c63ff"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(569.2 445.552)" fill="#6c63ff"/><circle cx="3.648" cy="3.648" r="3.648" transform="translate(581.358 445.552)" fill="#6c63ff"/><g transform="translate(933.489 421.851)"><path d="M0,0H88.949V88.949H0Z" fill="none"/><path d="M39.062,2A37.062,37.062,0,0,1,76.125,39.062a37.06,37.06,0,1,1-74.11,1.2L2,39.062l.015-1.038A37.061,37.061,0,0,1,39.062,2ZM39.1,50.181l-.471.026a3.706,3.706,0,0,0,0,7.361l.434.026.471-.026a3.706,3.706,0,0,0,0-7.361Zm-.037-32.362a3.706,3.706,0,0,0-3.68,3.272l-.026.43Line truncated

Before

Width:  |  Height:  |  Size: 5.0 KiB

+206 -117
View File
@@ -1,143 +1,232 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>家庭记账列表</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f9f9f9;
margin: 20px;
}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>共享空间</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
background: #f6f6f7;
color: #111;
}
.record-list {
max-width: 600px;
margin: auto;
}
.page {
max-width: 420px;
margin: 0 auto;
padding: 16px;
background: #fff;
min-height: 100vh;
box-sizing: border-box;
}
.record {
display: flex;
align-items: center;
padding: 10px 15px;
margin-bottom: 8px;
background: #fff;
border-radius: 8px;
position: relative;
transition: background 0.2s;
cursor: default;
}
h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
}
.record:hover {
background: #f2f2f2;
}
/* 按钮 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 40px;
padding: 0 14px;
border-radius: 8px;
background: #111;
color: #fff;
font-size: 14px;
cursor: pointer;
margin-top: 12px;
border: 1px solid #111;
}
.category-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #eee;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-right: 12px;
}
.btn-outline {
background: transparent;
color: #111;
}
.info {
flex: 1;
display: flex;
flex-direction: column;
}
.input-box {
width: 100%;
padding: 10px;
border-radius: 6px;
border: 1px solid #ddd;
margin-top: 12px;
font-size: 14px;
}
.name {
font-weight: 600;
font-size: 16px;
color: #333;
}
.list-item {
display: flex;
justify-content: space-between;
padding: 14px 0;
border-bottom: 1px solid #eee;
}
.remark {
font-size: 12px;
color: #888;
display: none;
margin-top: 2px;
}
.switcher {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.record:hover .remark {
display: block;
}
.switcher button {
flex: 1;
height: 36px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
border: 1px solid #ccc;
background: #fafafa;
}
.right {
display: flex;
align-items: center;
gap: 8px;
}
.switcher button.active {
background: #111;
color: #fff;
border-color: #111;
}
.amount {
font-weight: 600;
font-size: 16px;
}
/* 弹窗 */
.modal-mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.45);
display: none;
align-items: center;
justify-content: center;
}
.amount.income {
color: green;
}
.modal {
background: #fff;
padding: 20px;
width: 100%;
max-width: 360px;
text-align: center;
border: 1px solid #ddd;
border-radius: 0;
}
.amount.expense {
color: red;
}
.member-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: #2196f3;
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
.qr-box {
width: 180px;
height: 180px;
background: #eee;
margin: 16px auto;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="record-list">
<div class="record">
<div class="category-icon">🛒</div>
<div class="info">
<div class="name">购物</div>
<div class="remark">买菜和日用品</div>
</div>
<div class="right">
<div class="amount expense">-50.00</div>
<div class="member-avatar">爸</div>
</div>
<div class="page">
<h1>共享空间(预览)</h1>
<div class="switcher">
<button id="btnNotJoined" class="active" onclick="showPage('not')">未加入</button>
<button id="btnJoined" onclick="showPage('yes')">已加入</button>
</div>
<div class="record">
<div class="category-icon">🍔</div>
<div class="info">
<div class="name">午餐</div>
<div class="remark">外卖订单</div>
</div>
<div class="right">
<div class="amount expense">-30.00</div>
<div class="member-avatar">妈</div>
</div>
<!-- 未加入 -->
<div id="notJoined">
<p>你尚未加入共享空间,你可以创建自己的空间或加入他人的。</p>
<div class="btn" onclick="openInviteModal()">生成邀请码</div>
<div class="btn-outline btn" onclick="openScanModal()">扫码加入</div>
<div class="btn btn-outline" onclick="toggleJoinInput()">输入邀请码加入</div>
<input id="inviteInput" type="text" class="input-box" placeholder="邀请码" style="display:none;" />
<div id="joinBtn" class="btn" style="display:none;">加入</div>
</div>
<div class="record">
<div class="category-icon">💰</div>
<div class="info">
<div class="name">工资</div>
<div class="remark">11月工资到账</div>
</div>
<div class="right">
<div class="amount income">+5000.00</div>
<div class="member-avatar">我</div>
</div>
<!-- 已加入 -->
<div id="joined" style="display:none;">
<p>你已加入共享空间,可查看成员、权限和邀请他人加入。</p>
<h3 style="margin-top:20px; font-size:16px; font-weight:600;">成员列表</h3>
<div class="list-item"><span>张三</span><span>管理员</span></div>
<div class="list-item"><span>李四</span><span>成员</span></div>
<div class="list-item"><span>王五</span><span>成员</span></div>
<div class="btn" onclick="openInviteModal()">邀请新成员</div>
</div>
</div>
<!-- 邀请二维码弹窗 -->
<div id="inviteModal" class="modal-mask">
<div class="modal">
<h3>邀请码 / 二维码</h3>
<div id="qrBox" class="qr-box">二维码生成中…</div>
<div class="btn" onclick="closeInviteModal()">关闭</div>
</div>
</div>
<!-- 扫码加入弹窗(假装相机界面) -->
<div id="scanModal" class="modal-mask">
<div class="modal">
<h3>扫码加入</h3>
<div class="qr-box">[ 相机画面模拟区域 ]</div>
<p style="font-size:12px;color:#666;">打开摄像头并扫描共享空间二维码</p>
<div class="btn" onclick="closeScanModal()">关闭</div>
</div>
</div>
<script>
function showPage(type) {
const not = document.getElementById("notJoined");
const yes = document.getElementById("joined");
const btnNot = document.getElementById("btnNotJoined");
const btnYes = document.getElementById("btnJoined");
if (type === 'not') {
not.style.display = 'block';
yes.style.display = 'none';
btnNot.classList.add('active');
btnYes.classList.remove('active');
} else {
not.style.display = 'none';
yes.style.display = 'block';
btnYes.classList.add('active');
btnNot.classList.remove('active');
}
}
// ----------------- 邀请码 / 二维码弹窗 -----------------
function openInviteModal() {
const modal = document.getElementById('inviteModal');
const qr = document.getElementById('qrBox');
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
qr.innerHTML = `二维码:${code}`;
modal.style.display = 'flex';
}
function closeInviteModal() {
document.getElementById('inviteModal').style.display = 'none';
}
// ----------------- 扫码加入弹窗 -----------------
function openScanModal() {
document.getElementById('scanModal').style.display = 'flex';
}
function closeScanModal() {
document.getElementById('scanModal').style.display = 'none';
}
function toggleJoinInput() {
const input = document.getElementById("inviteInput");
const btn = document.getElementById("joinBtn");
const show = input.style.display === 'none';
input.style.display = show ? 'block' : 'none';
btn.style.display = show ? 'block' : 'none';
}
</script>
</body>
</html>