diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/BalanceCard.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/BalanceCard.tsx
index 5add6f48..220f7b29 100644
--- a/code/base-project/base-app/src/app/(tabs)/money/components/BalanceCard.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/money/components/BalanceCard.tsx
@@ -12,9 +12,9 @@ function BalanceCard() {
{/* 总余额 */}
- 总余额 (CNY)
-
- ¥ 12,322.67
+ 总余额
+
+ 12,322.67
@@ -22,12 +22,12 @@ function BalanceCard() {
{/* 收入 */}
-
+
收入
↑ +10%
-
- ¥ 12,232.00
+
+ 12,232.00
@@ -36,12 +36,12 @@ function BalanceCard() {
{/* 支出 */}
-
+
支出
↓ -20%
-
- ¥ 1,980.00
+
+ 1,980.00
diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/MoneyIcon.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/MoneyIcon.tsx
index 6247202b..7e920257 100644
--- a/code/base-project/base-app/src/app/(tabs)/money/components/MoneyIcon.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/money/components/MoneyIcon.tsx
@@ -14,7 +14,7 @@ const iconMap: Record = {
const DefaultIcon = IconLingshi
// 根据类别 name 返回图标组件
-export function getCategoryIcon(name: string, size = 24,type: , color?: string) {
+export function getCategoryIcon(name: string, size = 24, color?: string) {
const IconComponent = iconMap[name] || DefaultIcon
return
}
diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx
index 273fbf39..7f89d975 100644
--- a/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx
@@ -39,7 +39,7 @@ const sections: DaySection[] = [
{ id: '5', title: '电影票', description: '周末电影', amount: 45, type: 'expense' },
{ id: '6', title: '兼职收入', description: '家教', amount: 1200, type: 'income' },
],
- },
+ }
]
export default function StaticTransactions() {
@@ -70,12 +70,12 @@ export default function StaticTransactions() {
{section.data.map(item => (
{/* 图标 */}
{/* */}
-
+
{/* 标题 + 描述 */}
diff --git a/code/base-project/base-app/src/app/(tabs)/money/index.tsx b/code/base-project/base-app/src/app/(tabs)/money/index.tsx
index d8d6be1c..4264c4fd 100644
--- a/code/base-project/base-app/src/app/(tabs)/money/index.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/money/index.tsx
@@ -1,59 +1,126 @@
+import type { LayoutChangeEvent } from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
-import React, { useState } from 'react'
-import { ScrollView, Text, View } from 'react-native'
+import React, { useRef, useState } from 'react'
+import { Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LucideIcon } from '@/components/common/ao'
-
-import BalanceCard from './components/BalanceCard'
-import TabSelector from './components/TabSelector'
-import TransactionsList from './components/TransactionsList'
+import { IconJiaotong } from '@/components/iconfont'
+import { Grid, GridItem } from '@/components/ui/grid'
+import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
export default function Money() {
const insets = useSafeAreaInsets()
- const [selected, setSelected] = useState(0)
-
+ const [gradientHeight, setGradientHeight] = useState(200) // 默认渐变高度
+ const gridLayout = useRef({ y: 0, height: 0 })
+ const onGridLayout = (e: LayoutChangeEvent) => {
+ const { y, height } = e.nativeEvent.layout
+ gridLayout.current = { y, height }
+ setGradientHeight(y + height / 2 + 16 + insets.top)
+ }
return (
-
- {/* 顶部固定栏 */}
-
+
+
+ {/* 页面内容 */}
+
- 2025/11/10
-
+ 2025/11/10
+
-
+
- 筛选
- setSelected(tab)}
- />
-
+
+
+
+ 余额
+ ↑20%
+
+
+ 22313.00
+
+
- {/* 可滚动内容 */}
-
-
-
-
-
+
+
+
+ 收入
+ ↑20%
+
+ 11.0
+
+
+
+ 支出
+ ↑20%
+
+ 11.0
+
+
+
+ 结余
+ ↑20%
+
+ 11.0
+
+
+
+
+ 预算
+
+
+
+
+
+ 点击设置
+
+
+
+
+
+ {[...Array.from({ length: 5 })].map((_, i) => (
+
+
+ 账单
+
+ ))}
+
+
+
+
+
)
}
diff --git a/code/base-project/base-app/src/app/(tabs)/money/index2.tsx b/code/base-project/base-app/src/app/(tabs)/money/index2.tsx
new file mode 100644
index 00000000..6a6e3712
--- /dev/null
+++ b/code/base-project/base-app/src/app/(tabs)/money/index2.tsx
@@ -0,0 +1,90 @@
+// import { LinearGradient } from 'expo-linear-gradient'
+// import React from 'react'
+// import { Dimensions, View } from 'react-native'
+// import { useSafeAreaInsets } from 'react-native-safe-area-context'
+
+// export default function Money() {
+// const insets = useSafeAreaInsets()
+// const { height } = Dimensions.get('window')
+// const gradientHeight = height / 3 // 渐变高度 1/3
+
+// return (
+//
+// {/* 渐变层 */}
+//
+// {/* 可以在这里放置顶部卡片或统计信息 */}
+//
+//
+// )
+// }
+import { LinearGradient } from 'expo-linear-gradient'
+import React, { useState } from 'react'
+import { ScrollView, Text, View } from 'react-native'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+import { LucideIcon } from '@/components/common/ao'
+
+import BalanceCard from './components/BalanceCard'
+import TabSelector from './components/TabSelector'
+import TransactionsList from './components/TransactionsList'
+
+export default function Money() {
+ const insets = useSafeAreaInsets()
+ const [selected, setSelected] = useState(0)
+
+ return (
+
+
+
+
+ 2025/11/10
+
+
+
+
+ 筛选
+
+
+
+ setSelected(tab)}
+ />
+
+
+ {/* 可滚动内容 */}
+
+
+
+
+
+ )
+}
diff --git a/code/base-project/base-app/src/components/ui/grid/index.tsx b/code/base-project/base-app/src/components/ui/grid/index.tsx
new file mode 100644
index 00000000..ecff1b1d
--- /dev/null
+++ b/code/base-project/base-app/src/components/ui/grid/index.tsx
@@ -0,0 +1,283 @@
+import React, {
+ useState,
+ createContext,
+ useContext,
+ useMemo,
+ forwardRef,
+} from 'react';
+import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
+import { View, Dimensions, Platform, ViewProps } from 'react-native';
+import { gridStyle, gridItemStyle } from './styles';
+import { cssInterop } from 'nativewind';
+import {
+ useBreakpointValue,
+ getBreakPointValue,
+} from '@gluestack-ui/utils/hooks';
+const { width: DEVICE_WIDTH } = Dimensions.get('window');
+const GridContext = createContext({});
+function arrangeChildrenIntoRows({
+ childrenArray,
+ colSpanArr,
+ numColumns,
+}: {
+ childrenArray: React.ReactNode[];
+ colSpanArr: number[];
+ numColumns: number;
+}) {
+ let currentRow = 1;
+ let currentRowTotalColSpan = 0;
+ // store how many items in each row
+ const rowItemsCount: {
+ [key: number]: number[];
+ } = {};
+ for (let i = 0; i < childrenArray.length; i++) {
+ const colSpan = colSpanArr[i];
+ // if current row is full, go to next row
+ if (currentRowTotalColSpan + colSpan > numColumns) {
+ currentRow++;
+ currentRowTotalColSpan = colSpan;
+ } else {
+ // if current row is not full, add colSpan to current row
+ currentRowTotalColSpan += colSpan;
+ }
+ rowItemsCount[currentRow] = rowItemsCount[currentRow]
+ ? [...rowItemsCount[currentRow], i]
+ : [i];
+ }
+ return rowItemsCount;
+}
+function generateResponsiveNumColumns({ gridClass }: { gridClass: string }) {
+ const gridClassNamePattern = /\b(?:\w+:)?grid-cols-?\d+\b/g;
+ const numColumns = gridClass?.match(gridClassNamePattern);
+ if (!numColumns) {
+ return 12;
+ }
+ const regex = /^(?:(\w+):)?grid-cols-?(\d+)$/;
+ const result: any = {};
+ numColumns.forEach((classname) => {
+ const match = classname.match(regex);
+ if (match) {
+ const prefix = match[1] || 'default';
+ const value = parseInt(match[2], 10);
+ result[prefix] = value;
+ }
+ });
+ return result;
+}
+function generateResponsiveColSpans({
+ gridItemClassName,
+}: {
+ gridItemClassName: string;
+}) {
+ const gridClassNamePattern = /\b(?:\w+:)?col-span-?\d+\b/g;
+ const colSpan: any = gridItemClassName?.match(gridClassNamePattern);
+ if (!colSpan) {
+ return 1;
+ }
+ const regex = /^(?:(\w+):)?col-span-?(\d+)$/;
+ const result: any = {};
+ colSpan.forEach((classname: any) => {
+ const match = classname.match(regex);
+ if (match) {
+ const prefix = match[1] || 'default';
+ const value = parseInt(match[2], 10);
+ result[prefix] = value;
+ }
+ });
+ return result;
+}
+type IGridProps = ViewProps &
+ VariantProps & {
+ gap?: number;
+ rowGap?: number;
+ columnGap?: number;
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
+ padding?: number;
+ paddingLeft?: number;
+ paddingRight?: number;
+ paddingStart?: number;
+ paddingEnd?: number;
+ borderWidth?: number;
+ borderLeftWidth?: number;
+ borderRightWidth?: number;
+ _extra: {
+ className: string;
+ };
+ };
+const Grid = forwardRef, IGridProps>(
+ function Grid({ className, _extra, children, ...props }, ref) {
+ const [calculatedWidth, setCalculatedWidth] = useState(null);
+ const gridClass = _extra?.className;
+ const obj = generateResponsiveNumColumns({ gridClass });
+ const responsiveNumColumns: any = useBreakpointValue(obj);
+ const itemsPerRow = useMemo(() => {
+ // get the colSpan of each child
+ const colSpanArr = React.Children.map(children, (child: any) => {
+ const gridItemClassName = child?.props?._extra?.className;
+ const colSpan2 = getBreakPointValue(
+ generateResponsiveColSpans({ gridItemClassName }),
+ DEVICE_WIDTH
+ );
+ const colSpan = colSpan2 ? colSpan2 : 1;
+ if (colSpan > responsiveNumColumns) {
+ return responsiveNumColumns;
+ }
+ return colSpan;
+ });
+ const childrenArray = React.Children.toArray(children);
+ const rowItemsCount = arrangeChildrenIntoRows({
+ childrenArray,
+ colSpanArr,
+ numColumns: responsiveNumColumns,
+ });
+ return rowItemsCount;
+ }, [responsiveNumColumns, children]);
+ const childrenWithProps = React.Children.map(children, (child, index) => {
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child, { key: index, index: index } as any);
+ }
+ return child;
+ });
+ const gridClassMerged = `${Platform.select({
+ web: gridClass ?? '',
+ })}`;
+ const contextValue = useMemo(() => {
+ return {
+ calculatedWidth,
+ numColumns: responsiveNumColumns,
+ itemsPerRow,
+ flexDirection: props?.flexDirection || 'row',
+ gap: props?.gap || 0,
+ columnGap: props?.columnGap || 0,
+ };
+ }, [calculatedWidth, itemsPerRow, responsiveNumColumns, props]);
+ const borderLeftWidth = props?.borderLeftWidth || props?.borderWidth || 0;
+ const borderRightWidth = props?.borderRightWidth || props?.borderWidth || 0;
+ const borderWidthToSubtract = borderLeftWidth + borderRightWidth;
+ return (
+
+ {
+ const paddingLeftToSubtract =
+ props?.paddingStart || props?.paddingLeft || props?.padding || 0;
+ const paddingRightToSubtract =
+ props?.paddingEnd || props?.paddingRight || props?.padding || 0;
+ const gridWidth =
+ Math.floor(event.nativeEvent.layout.width) -
+ paddingLeftToSubtract -
+ paddingRightToSubtract -
+ borderWidthToSubtract;
+ setCalculatedWidth(gridWidth);
+ }}
+ {...props}
+ >
+ {calculatedWidth && childrenWithProps}
+
+
+ );
+ }
+);
+cssInterop(Grid, {
+ className: {
+ target: 'style',
+ nativeStyleToProp: {
+ gap: 'gap',
+ rowGap: 'rowGap',
+ columnGap: 'columnGap',
+ flexDirection: 'flexDirection',
+ padding: 'padding',
+ paddingLeft: 'paddingLeft',
+ paddingRight: 'paddingRight',
+ paddingStart: 'paddingStart',
+ paddingEnd: 'paddingEnd',
+ borderWidth: 'borderWidth',
+ borderLeftWidth: 'borderLeftWidth',
+ borderRightWidth: 'borderRightWidth',
+ },
+ },
+});
+type IGridItemProps = ViewProps &
+ VariantProps & {
+ index?: number;
+ _extra: {
+ className: string;
+ };
+ };
+const GridItem = forwardRef, IGridItemProps>(
+ function GridItem({ className, _extra, ...props }, ref) {
+ const {
+ calculatedWidth,
+ numColumns,
+ itemsPerRow,
+ flexDirection,
+ gap,
+ columnGap,
+ } = useContext(GridContext);
+ const gridItemClass = _extra?.className;
+ const responsiveColSpan = (useBreakpointValue(
+ generateResponsiveColSpans({ gridItemClassName: gridItemClass })
+ ) ?? 1) as number;
+ const flexBasisValue = useMemo(() => {
+ if (!calculatedWidth || !numColumns || responsiveColSpan <= 0) {
+ return 'auto';
+ }
+ if (flexDirection?.includes('column')) {
+ return 'auto';
+ }
+ // Find which row this item is in
+ const row = Object.keys(itemsPerRow).find((key) => {
+ return itemsPerRow[key].includes(props?.index);
+ });
+ if (!row) {
+ return 'auto';
+ }
+ const rowColsCount = itemsPerRow[row]?.length || 1;
+ const space = columnGap || gap || 0;
+ // Calculate available width accounting for gaps
+ const totalGapWidth = space * (rowColsCount - 1);
+ const availableWidth = calculatedWidth - totalGapWidth;
+ // Calculate the width for this item based on its column span
+ const itemWidth = (availableWidth * responsiveColSpan) / numColumns;
+ // Return the width directly instead of percentage for better native compatibility
+ return Math.max(0, Math.floor(itemWidth));
+ }, [
+ calculatedWidth,
+ responsiveColSpan,
+ numColumns,
+ columnGap,
+ gap,
+ flexDirection,
+ itemsPerRow,
+ props?.index,
+ ]);
+ return (
+
+ );
+ }
+);
+Grid.displayName = 'Grid';
+GridItem.displayName = 'GridItem';
+export { Grid, GridItem };
diff --git a/code/base-project/base-app/src/components/ui/grid/index.web.tsx b/code/base-project/base-app/src/components/ui/grid/index.web.tsx
new file mode 100644
index 00000000..cad120ef
--- /dev/null
+++ b/code/base-project/base-app/src/components/ui/grid/index.web.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { gridStyle, gridItemStyle } from './styles';
+
+import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
+
+type IGridProps = React.ComponentPropsWithoutRef<'div'> &
+ VariantProps & {
+ gap?: number;
+ rowGap?: number;
+ columnGap?: number;
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
+ padding?: number;
+ paddingLeft?: number;
+ paddingRight?: number;
+ paddingStart?: number;
+ paddingEnd?: number;
+ _extra: {
+ className: string;
+ };
+ };
+
+const Grid = React.forwardRef(function Grid(
+ { className, _extra, ...props },
+ ref
+) {
+ const gridClass = _extra?.className;
+ const finalGridClass = gridClass ?? '';
+ return (
+
+ );
+});
+
+type IGridItemProps = React.ComponentPropsWithoutRef<'div'> &
+ VariantProps & {
+ index?: number;
+ _extra: {
+ className: string;
+ };
+ };
+const GridItem = React.forwardRef(
+ function GridItem({ className, _extra, ...props }, ref) {
+ const gridItemClass = _extra?.className;
+
+ const finalGridItemClass = gridItemClass ?? '';
+ return (
+
+ );
+ }
+);
+
+Grid.displayName = 'Grid';
+GridItem.displayName = 'GridItem';
+
+export { Grid, GridItem };
diff --git a/code/base-project/base-app/src/components/ui/grid/styles.tsx b/code/base-project/base-app/src/components/ui/grid/styles.tsx
new file mode 100644
index 00000000..0fe60e24
--- /dev/null
+++ b/code/base-project/base-app/src/components/ui/grid/styles.tsx
@@ -0,0 +1,15 @@
+import { tva } from '@gluestack-ui/utils/nativewind-utils';
+import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
+
+const gridBaseStyle = isWeb
+ ? 'grid grid-cols-12'
+ : 'box-border flex-row flex-wrap justify-start';
+const gridItemBaseStyle = isWeb ? 'w-auto col-span-1' : '';
+
+export const gridStyle = tva({
+ base: `w-full ${gridBaseStyle}`,
+});
+
+export const gridItemStyle = tva({
+ base: `w-full ${gridItemBaseStyle}`,
+});
diff --git a/code/base-project/base-app/src/components/ui/progress/index.tsx b/code/base-project/base-app/src/components/ui/progress/index.tsx
new file mode 100644
index 00000000..baa63b07
--- /dev/null
+++ b/code/base-project/base-app/src/components/ui/progress/index.tsx
@@ -0,0 +1,169 @@
+'use client';
+import React from 'react';
+import { createProgress } from '@gluestack-ui/core/progress/creator';
+import { View } from 'react-native';
+import { tva } from '@gluestack-ui/utils/nativewind-utils';
+import {
+ withStyleContext,
+ useStyleContext,
+} from '@gluestack-ui/utils/nativewind-utils';
+import { cssInterop } from 'nativewind';
+import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
+
+const SCOPE = 'PROGRESS';
+export const UIProgress = createProgress({
+ Root: withStyleContext(View, SCOPE),
+ FilledTrack: View,
+});
+
+cssInterop(UIProgress, { className: 'style' });
+cssInterop(UIProgress.FilledTrack, { className: 'style' });
+
+const progressStyle = tva({
+ base: 'bg-background-300 rounded-full w-full',
+ variants: {
+ orientation: {
+ horizontal: 'w-full',
+ vertical: 'h-full',
+ },
+ size: {
+ 'xs': 'h-1',
+ 'sm': 'h-2',
+ 'md': 'h-3',
+ 'lg': 'h-4',
+ 'xl': 'h-5',
+ '2xl': 'h-6',
+ },
+ },
+ compoundVariants: [
+ {
+ orientation: 'vertical',
+ size: 'xs',
+ class: 'h-full w-1 justify-end',
+ },
+ {
+ orientation: 'vertical',
+ size: 'sm',
+ class: 'h-full w-2 justify-end',
+ },
+ {
+ orientation: 'vertical',
+ size: 'md',
+ class: 'h-full w-3 justify-end',
+ },
+ {
+ orientation: 'vertical',
+ size: 'lg',
+ class: 'h-full w-4 justify-end',
+ },
+
+ {
+ orientation: 'vertical',
+ size: 'xl',
+ class: 'h-full w-5 justify-end',
+ },
+ {
+ orientation: 'vertical',
+ size: '2xl',
+ class: 'h-full w-6 justify-end',
+ },
+ ],
+});
+
+const progressFilledTrackStyle = tva({
+ base: 'bg-primary-500 rounded-full',
+ parentVariants: {
+ orientation: {
+ horizontal: 'w-full',
+ vertical: 'h-full',
+ },
+ size: {
+ 'xs': 'h-1',
+ 'sm': 'h-2',
+ 'md': 'h-3',
+ 'lg': 'h-4',
+ 'xl': 'h-5',
+ '2xl': 'h-6',
+ },
+ },
+ parentCompoundVariants: [
+ {
+ orientation: 'vertical',
+ size: 'xs',
+ class: 'h-full w-1',
+ },
+ {
+ orientation: 'vertical',
+ size: 'sm',
+ class: 'h-full w-2',
+ },
+ {
+ orientation: 'vertical',
+ size: 'md',
+ class: 'h-full w-3',
+ },
+ {
+ orientation: 'vertical',
+ size: 'lg',
+ class: 'h-full w-4',
+ },
+
+ {
+ orientation: 'vertical',
+ size: 'xl',
+ class: 'h-full w-5',
+ },
+ {
+ orientation: 'vertical',
+ size: '2xl',
+ class: 'h-full w-6',
+ },
+ ],
+});
+
+type IProgressProps = VariantProps &
+ React.ComponentProps;
+type IProgressFilledTrackProps = VariantProps &
+ React.ComponentProps;
+
+const Progress = React.forwardRef<
+ React.ComponentRef,
+ IProgressProps
+>(function Progress(
+ { className, size = 'md', orientation = 'horizontal', ...props },
+ ref
+) {
+ return (
+
+ );
+});
+
+const ProgressFilledTrack = React.forwardRef<
+ React.ComponentRef,
+ IProgressFilledTrackProps
+>(function ProgressFilledTrack({ className, ...props }, ref) {
+ const { size: parentSize, orientation: parentOrientation } =
+ useStyleContext(SCOPE);
+
+ return (
+
+ );
+});
+
+export { Progress, ProgressFilledTrack };
diff --git a/code/base-project/base-app/test2.html b/code/base-project/base-app/test2.html
index 6851468c..15185663 100644
--- a/code/base-project/base-app/test2.html
+++ b/code/base-project/base-app/test2.html
@@ -1,118 +1,142 @@
-
+
-
-记账卡片优化版
+分类共享交互演示
-
-
-
¥ 8,520.00
+
记账分类共享交互演示
-
-
-
收入
-
¥ 12,000.00
-
+15% ↑
-
-
-
支出
-
¥ 3,480.00
-
-5% ↓
-
+
+
操作区(模拟修改)
+
选择用户修改系统分类或隐藏/重命名:
+
+
+
+
+
+
+
+
+
+