This commit is contained in:
oneao committed 2025-11-12 17:31:12 +08:00
1 parent b8a6b15355
commit 95d645f572
13 files changed
+660 -63

No files matched your search

@@ -1,28 +1,19 @@
import { ScrollView, Text } from 'react-native'
import { AButton } from '@/components/common/ao'
import { useUserStore } from '@/store/user'
import type { DateType } from 'react-native-ui-datepicker'
import { useState } from 'react'
import DateTimePicker, { useDefaultStyles } from 'react-native-ui-datepicker'
export default function Chat() {
const userStore = useUserStore()
export default function Calendar() {
const defaultStyles = useDefaultStyles()
const [selected, setSelected] = useState<DateType>()
return (
<ScrollView contentContainerStyle={{ padding: 16, marginTop: 22 }}>
<AButton
onPress={() => {
userStore.setUserInfo({
...(userStore.userInfo as any),
token: `test-token-${Date.now()}`,
})
}}
>
测试
</AButton>
<Text style={{ marginTop: 12 }}>
当前 token:
{' '}
{userStore.userInfo?.token ?? '(无)'}
</Text>
</ScrollView>
<DateTimePicker
mode="single"
date={selected}
onChange={({ date }) => setSelected(date)}
styles={defaultStyles}
className="mt-22"
/>
)
}
@@ -0,0 +1,68 @@
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,
},
})
@@ -0,0 +1,54 @@
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,17 +1,17 @@
import type { MoneyCategoryResult } from '@/request/api/money'
import type { MoneyCategory } from '@/types/money'
import { DeleteIcon } from 'lucide-react-native'
import Big from 'big.js'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { AInput } from '@/components/common/ao'
import { AInput, LucideIcon } from '@/components/common/ao'
import SegmentedTabs from '@/components/common/ao/SegmentedTabs'
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerFooter,
DrawerHeader,
} from '@/components/ui/drawer'
import { Grid, GridItem } from '@/components/ui/grid'
@@ -31,24 +31,24 @@ const keys: KeydownItem[] = [
{ key: '1', content: <Text className="text-2xl">1</Text> },
{ key: '2', content: <Text className="text-2xl">2</Text> },
{ key: '3', content: <Text className="text-2xl">3</Text> },
{ key: 'del', content: <DeleteIcon strokeWidth={1} /> },
{ key: 'del', content: <LucideIcon icon="Delete" size={22} strokeWidth={1.5} clickable={false} /> },
{ key: '4', content: <Text className="text-2xl">4</Text> },
{ key: '5', content: <Text className="text-2xl">5</Text> },
{ key: '6', content: <Text className="text-2xl">6</Text> },
{ key: '+', content: <Text className="text-2xl">+</Text> },
{ key: '+', content: <LucideIcon icon="Plus" size={22} strokeWidth={1.5} clickable={false} /> },
{ key: '7', content: <Text className="text-2xl">7</Text> },
{ key: '8', content: <Text className="text-2xl">8</Text> },
{ key: '9', content: <Text className="text-2xl">9</Text> },
{ key: '-', content: <Text className="text-2xl">-</Text> },
{ key: 'clear', content: (
{ key: '-', content: <LucideIcon icon="Minus" size={22} strokeWidth={1.5} clickable={false} /> },
{ key: 'repeat', content: (
<View className="w-full h-full flex items-center justify-center">
<Text className="text-2xl text-red-500">AC</Text>
<Text className="text-xl text-gray-600">再记</Text>
</View>
) },
{ key: '0', content: <Text className="text-2xl">0</Text> },
{ key: '.', content: <Text>.</Text> },
{ key: '.', content: <LucideIcon icon="Dot" size={22} strokeWidth={1.5} clickable={false} /> },
{ key: '/', content: (
<View className="w-full h-full bg-amber-100 flex items-center justify-center">
<View className="w-full h-full flex items-center justify-center">
<Text className="text-2xl">完成</Text>
</View>
) },
@@ -62,6 +62,9 @@ export default function AddMoneyDrawer({
const [selectedIndex, setSelectedIndex] = useState(0)
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null)
const [showKeydown, setShowKeydown] = useState(false)
const [keydownInput, setKeydownInput] = useState<string>('')
const [result, setResult] = useState('0')
const [date, setDate] = useState(dayjs())
const [categories, setCategories] = useState<MoneyCategoryResult>({
income: [],
@@ -79,7 +82,7 @@ export default function AddMoneyDrawer({
try {
const res = await getMoneyCategoryApi()
if (res.isSuccess) {
setCategories(res.data)
// setCategories(res.data)
}
}
catch (err) {
@@ -90,17 +93,161 @@ export default function AddMoneyDrawer({
}
}, [isOpen])
const handlePress = (key: string | number, idx: number) => {
console.log('点击了格子 key:', key, '索引:', idx)
function onMainDrawerClose() {
setShowKeydown(false)
onClose()
}
const handlePress = (key: string | number) => {
setKeydownInput((prev) => {
let newInput = prev
if (key === 'del') {
newInput = prev.slice(0, -1)
}
else if (key === '/') {
// 完成操作,可以在这里提交或关闭键盘
return prev
}
else {
const keyStr = key.toString()
// 检查是否要输入运算符(+ 或 -)
if (keyStr === '+' || keyStr === '-') {
// 获取最后一个字符
const lastChar = prev.slice(-1)
// 如果最后一个字符已经是运算符,则替换而不是追加
if (lastChar === '+' || lastChar === '-') {
newInput = prev.slice(0, -1) + keyStr
}
else {
newInput = prev + keyStr
}
}
// 检查是否要输入小数点(.)
else if (keyStr === '.') {
// 检查当前数字是否已经包含小数点
const parts = prev.split(/[+-]/)
const currentNumber = parts[parts.length - 1]
// 如果当前数字中还没有小数点,可以添加
if (!currentNumber.includes('.')) {
// 检查当前数字的小数位数(应该是0,因为还没有小数点)
if (currentNumber === '' || /[+-]/.test(prev.slice(-1))) {
newInput = `${prev}0.`
}
else {
newInput = `${prev}.`
}
}
else {
// 已经有小数点了,不重复添加
newInput = prev
}
}
// 处理数字输入
else if (/^\d$/.test(keyStr)) {
// 检查当前数字的小数位数
const parts = prev.split(/[+-]/)
const currentNumber = parts[parts.length - 1]
// 如果当前数字包含小数点,检查小数位数
if (currentNumber.includes('.')) {
const decimalParts = currentNumber.split('.')
// 如果小数部分已经达到2位,不允许继续输入
if (decimalParts.length > 1 && decimalParts[1].length >= 2) {
newInput = prev // 不改变输入
}
else {
newInput = prev + keyStr
}
}
else {
// 没有小数点,正常输入数字
newInput = prev + keyStr
}
}
else {
// 对于其他字符,正常追加
newInput = prev + keyStr
}
}
// 计算结果
try {
const computed = calculateWithBig(newInput)
setResult(computed)
}
catch (e) {
setResult('0')
}
return newInput
})
}
function calculateWithBig(expression: string): string {
if (!expression.trim())
return '0'
// 移除表达式末尾的运算符(+或-),避免不完整的表达式
let cleanExpression = expression.trim()
const lastChar = cleanExpression.slice(-1)
if (lastChar === '+' || lastChar === '-') {
cleanExpression = cleanExpression.slice(0, -1)
}
if (!cleanExpression)
return '0'
try {
// 使用更智能的解析方式,支持基本的加减运算
const numbers = cleanExpression.split(/[+-]/).filter(Boolean)
const operators = cleanExpression.split(/[^+-]/).filter(Boolean)
if (numbers.length === 0)
return '0'
let result = new Big(numbers[0])
for (let i = 0; i < operators.length; i++) {
const operator = operators[i]
const nextNumber = new Big(numbers[i + 1])
if (operator === '+') {
result = result.plus(nextNumber)
}
else if (operator === '-') {
result = result.minus(nextNumber)
}
}
// 限制结果为最多两位小数
return result.toFixed(2)
}
catch (error) {
// 如果解析失败,尝试只计算数字部分
const tokens = expression.match(/[+-]?\d+(\.\d+)?/g)
if (!tokens || tokens.length === 0)
return '0'
let total = new Big(0)
tokens.forEach((token) => {
total = total.plus(new Big(token))
})
// 限制结果为最多两位小数
return total.toFixed(2)
}
}
return (
<>
<Drawer
isOpen={isOpen}
size="full"
anchor="bottom"
onClose={onClose}
onClose={onMainDrawerClose}
>
<DrawerBackdrop />
<DrawerContent style={{ padding: 0, paddingLeft: 16, paddingRight: 16 }}>
@@ -116,7 +263,7 @@ export default function AddMoneyDrawer({
style={{ width: 180 }}
className="mx-auto"
/>
<Pressable onPress={onClose}>
<Pressable onPress={onMainDrawerClose}>
<Text className="text-gray-500 font-medium">取消</Text>
</Pressable>
</DrawerHeader>
@@ -167,6 +314,7 @@ export default function AddMoneyDrawer({
isOpen={showKeydown}
size="md"
anchor="bottom"
onClose={() => setShowKeydown(false)}
>
<DrawerContent
style={{
@@ -180,7 +328,21 @@ export default function AddMoneyDrawer({
>
<View className="flex-1">
<View className="p-4">
<Text className="text-3xl text-blue-700 text-right">0.00</Text>
<View className="flex flex-row items-center justify-between">
<Pressable
className="flex flex-row items-center gap-1.5 px-2 py-1.5 rounded-md bg-gray-50 active:opacity-70"
>
<LucideIcon icon="Calendar" size={16} color="#60a5fa" />
<Text className="text-gray-600 text-sm">2024/11/05</Text>
<LucideIcon icon="ChevronDown" size={12} color="#aaa" />
</Pressable>
<Text className="text-3xl text-blue-700 text-right">{result}</Text>
</View>
<View className="mt-1">
<Text className="text-gray-400 text-right">{keydownInput}</Text>
</View>
<AInput
value=""
placeholder="备注"
@@ -200,7 +362,7 @@ export default function AddMoneyDrawer({
return (
<Pressable
key={item.key}
onPress={() => handlePress(item.key, idx)}
onPress={() => handlePress(item.key)}
style={{
width: '25%',
height: '25%',
@@ -213,7 +375,22 @@ export default function AddMoneyDrawer({
borderColor: '#ddd',
}}
>
{item.content}
{({ pressed }) => {
const defaultBg = item.key === '/' ? '#e6d7b8' : '#fff'
const pressedBg = item.key === '/' ? '#f5e9d4' : '#f2f2f2'
return (
<>
<View
style={{
...StyleSheet.absoluteFillObject,
backgroundColor: pressed ? pressedBg : defaultBg,
}}
/>
{item.content}
</>
)
}}
</Pressable>
)
})}
@@ -223,6 +400,7 @@ export default function AddMoneyDrawer({
</View>
</DrawerContent>
</Drawer>
</>
)
}
@@ -10,6 +10,7 @@ interface LucideIconProps {
strokeWidth?: number
onPress?: (event: GestureResponderEvent) => void
style?: ViewStyle
clickable?: boolean // 新增参数,是否可点击,默认 true
}
export default function LucideIcon({
@@ -19,12 +20,18 @@ export default function LucideIcon({
strokeWidth = 2,
onPress,
style,
clickable = true,
}: LucideIconProps) {
const IconComponent = icons[icon]
if (!IconComponent)
return null
if (!clickable) {
return <IconComponent size={size} color={color} strokeWidth={strokeWidth} style={style} />
}
// clickable 为 true,包 Pressable
return (
<Pressable onPress={onPress} style={style}>
<IconComponent size={size} color={color} strokeWidth={strokeWidth} />
@@ -11,3 +11,34 @@ export interface MoneyCategoryResult {
export function getMoneyCategoryApi() {
return alovaInstance.Get<ResponseData<MoneyCategoryResult>>(`${API_PREFIX}/category`)
}
interface InsertMoneyRecordParams {
category_id: number
amount: string
record_time: string
remark: string
}
export function insertMoneyRecordApi(params: InsertMoneyRecordParams) {
return alovaInstance.Post<ResponseData<MoneyCategory>>(`${API_PREFIX}/insert`, params)
}
interface UpdateMoneyRecordParams {
id: number
category_id: number
amount: string
record_time: string
remark: string
}
export function updateMoneyRecordApi(params: UpdateMoneyRecordParams) {
return alovaInstance.Post<ResponseData<any>>(`${API_PREFIX}/update`, params)
}
interface DeleteMoneyRecordParams {
id: number
}
export function deleteMoneyRecordApi(params: DeleteMoneyRecordParams) {
return alovaInstance.Post<ResponseData<any>>(`${API_PREFIX}/delete`, params)
}
@@ -30,11 +30,11 @@ 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,
cacheLogger: false,
cacheFor: null,
responded: {
onSuccess: async (response, method: Method) => {
const resData = response.data as ResponseData<any>
@@ -154,7 +154,6 @@ const alovaInstance = createAlova({
return Promise.reject(new Error('未登录'))
}
method.config.cacheFor = 0
method.config.headers.token = useUserStore.getState().userInfo?.token ?? ''
},
})
@@ -6,3 +6,13 @@ export interface MoneyCategory {
sort_number: number
type: number
}
export interface MoneyRecord {
id: number
category_id: number
user_id: number
amount: string
remark: string
record_time: string
create_time: string
}