u
This commit is contained in:
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
|
||||
}
|
||||
Reference in new issue
Block a user