u
This commit is contained in:
1 parent
dee2eab404
commit
098a061308
10 files changed
+869
-155
No files matched your search
@@ -12,9 +12,9 @@ function BalanceCard() {
|
|||||||
|
|
||||||
{/* 总余额 */}
|
{/* 总余额 */}
|
||||||
<View className="items-center mb-4 mt-3">
|
<View className="items-center mb-4 mt-3">
|
||||||
<Text className="text-gray-500 text-sm font-medium mb-2">总余额 (CNY)</Text>
|
<Text className="text-gray-500 text-sm font-medium mb-2">总余额</Text>
|
||||||
<Text className="text-4xl font-black text-gray-800 text-center tracking-tight">
|
<Text className="text-2xl font-black text-gray-800 text-center tracking-tight">
|
||||||
¥ 12,322.67
|
12,322.67
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ function BalanceCard() {
|
|||||||
<View className="flex-row justify-between rounded-lg pt-4">
|
<View className="flex-row justify-between rounded-lg pt-4">
|
||||||
{/* 收入 */}
|
{/* 收入 */}
|
||||||
<View className="flex-1 items-center pr-3">
|
<View className="flex-1 items-center pr-3">
|
||||||
<View className="flex-row justify-center w-full mb-2">
|
<View className="flex-row justify-center items-center w-full mb-2">
|
||||||
<Text className="text-gray-600 text-sm font-medium">收入</Text>
|
<Text className="text-gray-600 text-sm font-medium">收入</Text>
|
||||||
<Text className="text-green-500 text-xs font-semibold ml-1">↑ +10%</Text>
|
<Text className="text-green-500 text-xs font-semibold ml-1">↑ +10%</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-2xl font-bold text-gray-800 text-center">
|
<Text className="text-base font-bold text-gray-800 text-center">
|
||||||
¥ 12,232.00
|
12,232.00
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -36,12 +36,12 @@ function BalanceCard() {
|
|||||||
|
|
||||||
{/* 支出 */}
|
{/* 支出 */}
|
||||||
<View className="flex-1 items-center pl-3">
|
<View className="flex-1 items-center pl-3">
|
||||||
<View className="flex-row justify-center w-full mb-2">
|
<View className="flex-row justify-center items-center w-full mb-2">
|
||||||
<Text className="text-gray-600 text-sm font-medium">支出</Text>
|
<Text className="text-gray-600 text-sm font-medium">支出</Text>
|
||||||
<Text className="text-red-500 text-xs font-semibold ml-1">↓ -20%</Text>
|
<Text className="text-red-500 text-xs font-semibold ml-1">↓ -20%</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-2xl font-bold text-gray-800 text-center">
|
<Text className="text-base font-bold text-gray-800 text-center">
|
||||||
¥ 1,980.00
|
1,980.00
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const iconMap: Record<string, React.ElementType> = {
|
|||||||
const DefaultIcon = IconLingshi
|
const DefaultIcon = IconLingshi
|
||||||
|
|
||||||
// 根据类别 name 返回图标组件
|
// 根据类别 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
|
const IconComponent = iconMap[name] || DefaultIcon
|
||||||
return <IconComponent size={size} color={color} />
|
return <IconComponent size={size} color={color} />
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ const sections: DaySection[] = [
|
|||||||
{ id: '5', title: '电影票', description: '周末电影', amount: 45, type: 'expense' },
|
{ id: '5', title: '电影票', description: '周末电影', amount: 45, type: 'expense' },
|
||||||
{ id: '6', title: '兼职收入', description: '家教', amount: 1200, type: 'income' },
|
{ id: '6', title: '兼职收入', description: '家教', amount: 1200, type: 'income' },
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function StaticTransactions() {
|
export default function StaticTransactions() {
|
||||||
@@ -70,12 +70,12 @@ export default function StaticTransactions() {
|
|||||||
{section.data.map(item => (
|
{section.data.map(item => (
|
||||||
<View
|
<View
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="flex-row items-center bg-white rounded-md p-4 shadow-md mb-4"
|
className="flex-row items-center p-2"
|
||||||
>
|
>
|
||||||
{/* 图标 */}
|
{/* 图标 */}
|
||||||
<View className="w-12 h-12 rounded-full bg-white shadow-md items-center justify-center mr-4">
|
<View className="w-12 h-12 rounded-full bg-white shadow-md items-center justify-center mr-4">
|
||||||
{/* <IconFont name="shucai" size={28} /> */}
|
{/* <IconFont name="shucai" size={28} /> */}
|
||||||
<IconFont name='jiaotong' size={28} />
|
<IconFont name="jiaotong" size={28} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 标题 + 描述 */}
|
{/* 标题 + 描述 */}
|
||||||
|
|||||||
@@ -1,59 +1,126 @@
|
|||||||
|
import type { LayoutChangeEvent } from 'react-native'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import React, { useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { ScrollView, Text, View } from 'react-native'
|
import { Text, View } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { LucideIcon } from '@/components/common/ao'
|
import { LucideIcon } from '@/components/common/ao'
|
||||||
|
import { IconJiaotong } from '@/components/iconfont'
|
||||||
import BalanceCard from './components/BalanceCard'
|
import { Grid, GridItem } from '@/components/ui/grid'
|
||||||
import TabSelector from './components/TabSelector'
|
import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
|
||||||
import TransactionsList from './components/TransactionsList'
|
|
||||||
|
|
||||||
export default function Money() {
|
export default function Money() {
|
||||||
const insets = useSafeAreaInsets()
|
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 (
|
return (
|
||||||
<LinearGradient
|
<View
|
||||||
colors={['#f2f6ff', '#ffffff']}
|
className="flex-1 relative bg-white"
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 0, y: 1 }}
|
|
||||||
className="flex-1"
|
|
||||||
style={{
|
style={{
|
||||||
paddingTop: insets.top,
|
paddingTop: insets.top,
|
||||||
paddingBottom: insets.bottom,
|
|
||||||
paddingLeft: 8,
|
|
||||||
paddingRight: 8,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 顶部固定栏 */}
|
<LinearGradient
|
||||||
<View style={{ zIndex: 10 }}>
|
colors={['#a3f7bf', '#d4f9e2', '#ffffff']}
|
||||||
|
locations={[0, 0.85, 1]}
|
||||||
|
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: gradientHeight,
|
||||||
|
zIndex: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 页面内容 */}
|
||||||
|
<View style={{ zIndex: 10, padding: 8 }}>
|
||||||
<View className="flex flex-row justify-between items-center py-2">
|
<View className="flex flex-row justify-between items-center py-2">
|
||||||
<View className="flex flex-row items-center gap-2">
|
<View className="flex flex-row items-center gap-2">
|
||||||
<Text className="font-bold text-lg">2025/11/10</Text>
|
<Text className="font-bold text-base">2025/11/10</Text>
|
||||||
<LucideIcon icon="CircleChevronDown" size={16} />
|
<LucideIcon icon="CircleChevronDown" size={14} />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-row items-center gap-2">
|
<View className="flex flex-row items-center gap-2">
|
||||||
<LucideIcon icon="SlidersHorizontal" />
|
<LucideIcon icon="SlidersHorizontal" />
|
||||||
<Text>筛选</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TabSelector
|
<View>
|
||||||
options={['明细', '视图']}
|
<View className="flex flex-col items-center">
|
||||||
selected={selected}
|
<Text className="text-gray-600 text-xs">
|
||||||
onSelect={tab => setSelected(tab)}
|
<Text>余额</Text>
|
||||||
/>
|
<Text className="text-green-500">↑20%</Text>
|
||||||
</View>
|
</Text>
|
||||||
|
<Text className="font-bold text-xl">
|
||||||
|
22313.00
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* 可滚动内容 */}
|
<View className="flex flex-row justify-between mt-2">
|
||||||
<ScrollView
|
<View className="flex flex-col items-center">
|
||||||
showsVerticalScrollIndicator={false}
|
<Text className="text-gray-600 text-xs">
|
||||||
contentContainerStyle={{ paddingBottom: 20 }}
|
<Text>收入</Text>
|
||||||
style={{ marginTop: 0 }}
|
<Text className="text-green-500">↑20%</Text>
|
||||||
>
|
</Text>
|
||||||
<BalanceCard />
|
<Text className="text-lg font-bold">11.0</Text>
|
||||||
<TransactionsList />
|
</View>
|
||||||
</ScrollView>
|
<View className="flex flex-col items-center">
|
||||||
</LinearGradient>
|
<Text className="text-gray-600 text-xs">
|
||||||
|
<Text>支出</Text>
|
||||||
|
<Text className="text-green-500">↑20%</Text>
|
||||||
|
</Text>
|
||||||
|
<Text className="text-lg font-bold">11.0</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex flex-col items-center">
|
||||||
|
<Text className="text-gray-600 text-xs">
|
||||||
|
<Text>结余</Text>
|
||||||
|
<Text className="text-green-500">↑20%</Text>
|
||||||
|
</Text>
|
||||||
|
<Text className="text-lg font-bold">11.0</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex flex-row items-center gap-4 mt-4 ">
|
||||||
|
<Text className="text-xs text-gray-600">预算</Text>
|
||||||
|
|
||||||
|
<View className="flex-1">
|
||||||
|
<Progress>
|
||||||
|
<ProgressFilledTrack />
|
||||||
|
</Progress>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text className="text-xs text-gray-600">点击设置</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
onLayout={onGridLayout}
|
||||||
|
className="mt-4 pt-2 pb-2 bg-white rounded-md"
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
_extra={{ className: 'grid-cols-5' }}
|
||||||
|
>
|
||||||
|
{[...Array.from({ length: 5 })].map((_, i) => (
|
||||||
|
<GridItem
|
||||||
|
key={i}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
_extra={{ className: 'col-span-1' }}
|
||||||
|
>
|
||||||
|
<IconJiaotong size={28} />
|
||||||
|
<Text className="text-xs mt-1 text-gray-500">账单</Text>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 (
|
||||||
|
// <View className="flex-1 relative">
|
||||||
|
// {/* 渐变层 */}
|
||||||
|
// <LinearGradient
|
||||||
|
// colors={['#e6f0ff', '#ffffff']} // 顶部蓝色 → 白色
|
||||||
|
// start={{ x: 0, y: 0 }}
|
||||||
|
// end={{ x: 0, y: 1 }}
|
||||||
|
// style={{
|
||||||
|
// position: 'absolute',
|
||||||
|
// top: 0,
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// height: gradientHeight,
|
||||||
|
// paddingTop: insets.top,
|
||||||
|
// paddingHorizontal: 8,
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// {/* 可以在这里放置顶部卡片或统计信息 */}
|
||||||
|
// </LinearGradient>
|
||||||
|
// </View>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
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 (
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#e6f0ff', '#ffffff']}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
className="flex-1"
|
||||||
|
style={{
|
||||||
|
paddingTop: insets.top,
|
||||||
|
paddingBottom: insets.bottom,
|
||||||
|
paddingLeft: 8,
|
||||||
|
paddingRight: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ zIndex: 10 }}>
|
||||||
|
<View className="flex flex-row justify-between items-center py-2">
|
||||||
|
<View className="flex flex-row items-center gap-2">
|
||||||
|
<Text className="font-bold text-lg">2025/11/10</Text>
|
||||||
|
<LucideIcon icon="CircleChevronDown" size={16} />
|
||||||
|
</View>
|
||||||
|
<View className="flex flex-row items-center gap-2">
|
||||||
|
<LucideIcon icon="SlidersHorizontal" />
|
||||||
|
<Text>筛选</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TabSelector
|
||||||
|
options={['明细', '视图']}
|
||||||
|
selected={selected}
|
||||||
|
onSelect={tab => setSelected(tab)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 可滚动内容 */}
|
||||||
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={{ paddingBottom: 20 }}
|
||||||
|
style={{ marginTop: 0 }}
|
||||||
|
>
|
||||||
|
<BalanceCard />
|
||||||
|
<TransactionsList />
|
||||||
|
</ScrollView>
|
||||||
|
</LinearGradient>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<any>({});
|
||||||
|
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<typeof gridStyle> & {
|
||||||
|
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<React.ComponentRef<typeof View>, IGridProps>(
|
||||||
|
function Grid({ className, _extra, children, ...props }, ref) {
|
||||||
|
const [calculatedWidth, setCalculatedWidth] = useState<number | null>(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 (
|
||||||
|
<GridContext.Provider value={contextValue}>
|
||||||
|
<View
|
||||||
|
ref={ref}
|
||||||
|
className={gridStyle({
|
||||||
|
class: className + ' ' + gridClassMerged,
|
||||||
|
})}
|
||||||
|
onLayout={(event) => {
|
||||||
|
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}
|
||||||
|
</View>
|
||||||
|
</GridContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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<typeof gridItemStyle> & {
|
||||||
|
index?: number;
|
||||||
|
_extra: {
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const GridItem = forwardRef<React.ComponentRef<typeof View>, 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 (
|
||||||
|
<View
|
||||||
|
ref={ref}
|
||||||
|
// @ts-expect-error : internal implementation for r-19/react-native-web
|
||||||
|
gridItemClass={gridItemClass}
|
||||||
|
className={gridItemStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width:
|
||||||
|
typeof flexBasisValue === 'number' ? flexBasisValue : undefined,
|
||||||
|
flexBasis:
|
||||||
|
typeof flexBasisValue === 'string' ? flexBasisValue : undefined,
|
||||||
|
flexShrink: 0,
|
||||||
|
flexGrow: 0,
|
||||||
|
},
|
||||||
|
props.style,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Grid.displayName = 'Grid';
|
||||||
|
GridItem.displayName = 'GridItem';
|
||||||
|
export { Grid, GridItem };
|
||||||
@@ -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<typeof gridStyle> & {
|
||||||
|
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<HTMLDivElement, IGridProps>(function Grid(
|
||||||
|
{ className, _extra, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const gridClass = _extra?.className;
|
||||||
|
const finalGridClass = gridClass ?? '';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={gridStyle({
|
||||||
|
class: className + ' ' + finalGridClass,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IGridItemProps = React.ComponentPropsWithoutRef<'div'> &
|
||||||
|
VariantProps<typeof gridItemStyle> & {
|
||||||
|
index?: number;
|
||||||
|
_extra: {
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const GridItem = React.forwardRef<HTMLDivElement, IGridItemProps>(
|
||||||
|
function GridItem({ className, _extra, ...props }, ref) {
|
||||||
|
const gridItemClass = _extra?.className;
|
||||||
|
|
||||||
|
const finalGridItemClass = gridItemClass ?? '';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={gridItemStyle({
|
||||||
|
class: className + ' ' + finalGridItemClass,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Grid.displayName = 'Grid';
|
||||||
|
GridItem.displayName = 'GridItem';
|
||||||
|
|
||||||
|
export { Grid, GridItem };
|
||||||
@@ -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}`,
|
||||||
|
});
|
||||||
@@ -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<typeof progressStyle> &
|
||||||
|
React.ComponentProps<typeof UIProgress>;
|
||||||
|
type IProgressFilledTrackProps = VariantProps<typeof progressFilledTrackStyle> &
|
||||||
|
React.ComponentProps<typeof UIProgress.FilledTrack>;
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIProgress>,
|
||||||
|
IProgressProps
|
||||||
|
>(function Progress(
|
||||||
|
{ className, size = 'md', orientation = 'horizontal', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIProgress
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={progressStyle({ size, orientation, class: className })}
|
||||||
|
context={{ size, orientation }}
|
||||||
|
orientation={orientation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProgressFilledTrack = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIProgress.FilledTrack>,
|
||||||
|
IProgressFilledTrackProps
|
||||||
|
>(function ProgressFilledTrack({ className, ...props }, ref) {
|
||||||
|
const { size: parentSize, orientation: parentOrientation } =
|
||||||
|
useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIProgress.FilledTrack
|
||||||
|
ref={ref}
|
||||||
|
className={progressFilledTrackStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
orientation: parentOrientation,
|
||||||
|
},
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Progress, ProgressFilledTrack };
|
||||||
@@ -1,118 +1,142 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>分类共享交互演示</title>
|
||||||
<title>记账卡片优化版</title>
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||||
margin: 0;
|
h2 { margin-top: 20px; }
|
||||||
font-family: "PingFang SC", "Helvetica", "Arial", sans-serif;
|
ul { list-style: none; padding-left: 0; }
|
||||||
background-color: #f0f2f5;
|
li { padding: 5px 10px; margin: 3px 0; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
|
||||||
display: flex;
|
.system { background-color: #d0ebff; }
|
||||||
justify-content: center;
|
.family { background-color: #ffe5d0; }
|
||||||
padding: 16px;
|
.user { background-color: #d0ffd8; }
|
||||||
}
|
.hidden { display: none; }
|
||||||
|
button { margin-left: 5px; }
|
||||||
.card {
|
.container { display: flex; gap: 50px; }
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
border-radius: 24px;
|
|
||||||
padding: 24px;
|
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item {
|
|
||||||
flex: 1;
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 14px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item .title {
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.8;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item .amount {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item .percent {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.percent.up {
|
|
||||||
color: #16c784; /* 上升绿色 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.percent.down {
|
|
||||||
color: #ff6b6b; /* 下降红色 */
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.balance {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
.summary-item .amount {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="card">
|
<h1>记账分类共享交互演示</h1>
|
||||||
<div class="card-header">账户余额</div>
|
|
||||||
<div class="balance">¥ 8,520.00</div>
|
|
||||||
|
|
||||||
<div class="summary">
|
<div class="container">
|
||||||
<div class="summary-item">
|
<div>
|
||||||
<div class="title">收入</div>
|
<h2>用户 A 视图</h2>
|
||||||
<div class="amount">¥ 12,000.00</div>
|
<ul id="userAList"></ul>
|
||||||
<div class="percent up">+15% ↑</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div class="summary-item">
|
<h2>用户 B 视图</h2>
|
||||||
<div class="title">支出</div>
|
<ul id="userBList"></ul>
|
||||||
<div class="amount">¥ 3,480.00</div>
|
|
||||||
<div class="percent down">-5% ↓</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2>操作区(模拟修改)</h2>
|
||||||
|
<p>选择用户修改系统分类或隐藏/重命名:</p>
|
||||||
|
<select id="selectUser">
|
||||||
|
<option value="A">用户 A</option>
|
||||||
|
<option value="B">用户 B</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectCategory">
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="text" id="newName" placeholder="新名字">
|
||||||
|
<button id="updateName">修改名字</button>
|
||||||
|
<button id="hideCategory">隐藏分类</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 系统默认分类
|
||||||
|
const systemCategories = [
|
||||||
|
{ id: 1, name: "餐饮" },
|
||||||
|
{ id: 2, name: "交通" },
|
||||||
|
{ id: 3, name: "购物" },
|
||||||
|
{ id: 4, name: "工资" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 家庭组 overlay(共享修改)
|
||||||
|
const familyCategoryStatus = {
|
||||||
|
// category_id: name
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用户个人状态
|
||||||
|
const userStatus = {
|
||||||
|
A: { hidden: [], customName: {} },
|
||||||
|
B: { hidden: [], customName: {} }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 填充选择分类下拉
|
||||||
|
const selectCategory = document.getElementById("selectCategory");
|
||||||
|
systemCategories.forEach(cat => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = cat.id;
|
||||||
|
opt.textContent = cat.name;
|
||||||
|
selectCategory.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染函数
|
||||||
|
function render() {
|
||||||
|
["A","B"].forEach(user => {
|
||||||
|
const listEl = document.getElementById(`user${user}List`);
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
|
||||||
|
systemCategories.forEach(cat => {
|
||||||
|
let displayName = cat.name;
|
||||||
|
let cssClass = "system";
|
||||||
|
|
||||||
|
// 家庭共享修改覆盖
|
||||||
|
if (familyCategoryStatus[cat.id]) {
|
||||||
|
displayName = familyCategoryStatus[cat.id];
|
||||||
|
cssClass = "family";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户个人自定义
|
||||||
|
if (userStatus[user].customName[cat.id]) {
|
||||||
|
displayName = userStatus[user].customName[cat.id];
|
||||||
|
cssClass = "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户隐藏
|
||||||
|
if (userStatus[user].hidden.includes(cat.id)) {
|
||||||
|
cssClass += " hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = displayName;
|
||||||
|
li.className = cssClass;
|
||||||
|
listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
|
||||||
|
// 修改名字
|
||||||
|
document.getElementById("updateName").onclick = () => {
|
||||||
|
const user = document.getElementById("selectUser").value;
|
||||||
|
const catId = parseInt(selectCategory.value);
|
||||||
|
const newName = document.getElementById("newName").value.trim();
|
||||||
|
if(!newName) return alert("请输入新名字");
|
||||||
|
|
||||||
|
if(user === "A") {
|
||||||
|
// 用户 A 修改 → 家庭共享
|
||||||
|
familyCategoryStatus[catId] = newName;
|
||||||
|
} else {
|
||||||
|
// 用户 B 修改 → 家庭共享
|
||||||
|
familyCategoryStatus[catId] = newName;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
document.getElementById("newName").value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏分类
|
||||||
|
document.getElementById("hideCategory").onclick = () => {
|
||||||
|
const user = document.getElementById("selectUser").value;
|
||||||
|
const catId = parseInt(selectCategory.value);
|
||||||
|
userStatus[user].hidden.push(catId);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user