feat: 轮播图

This commit is contained in:
jinyaqiu 2025-07-21 18:21:13 +08:00
parent 20c1b2b767
commit 80eaad039e
6 changed files with 157 additions and 33 deletions

View File

@ -5,21 +5,19 @@ import StoriesSvg from '@/assets/icons/svg/stories.svg';
import UsedStorageSvg from '@/assets/icons/svg/usedStorage.svg'; import UsedStorageSvg from '@/assets/icons/svg/usedStorage.svg';
import AskNavbar from '@/components/layout/ask'; import AskNavbar from '@/components/layout/ask';
import AlbumComponent from '@/components/owner/album'; import AlbumComponent from '@/components/owner/album';
import CategoryComponent from '@/components/owner/category'; import CarouselComponent from '@/components/owner/carousel';
import CountComponent from '@/components/owner/count';
import CreateCountComponent from '@/components/owner/createCount'; import CreateCountComponent from '@/components/owner/createCount';
import Ranking from '@/components/owner/ranking'; import Ranking from '@/components/owner/ranking';
import ResourceComponent from '@/components/owner/resource'; import ResourceComponent from '@/components/owner/resource';
import SettingModal from '@/components/owner/setting'; import SettingModal from '@/components/owner/setting';
import UserInfo from '@/components/owner/userName'; import UserInfo from '@/components/owner/userName';
import { formatDuration } from '@/components/utils/time';
import { checkAuthStatus } from '@/lib/auth'; import { checkAuthStatus } from '@/lib/auth';
import { fetchApi } from '@/lib/server-api-util'; import { fetchApi } from '@/lib/server-api-util';
import { CountData, UserInfoDetails } from '@/types/user'; import { CountData, UserInfoDetails } from '@/types/user';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FlatList, ScrollView, StyleSheet, View } from 'react-native'; import { FlatList, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function OwnerPage() { export default function OwnerPage() {
@ -97,27 +95,8 @@ export default function OwnerPage() {
<MoreArrowSvg /> <MoreArrowSvg />
</View> </View>
</View> </View>
{/* 数据统计 */}
<CountComponent
data={[{ title: t("generalSetting.totalVideo", { ns: "personal" }), number: countData?.counter?.total_count?.video_count || 0 }, { title: t("generalSetting.totalPhoto", { ns: "personal" }), number: countData?.counter?.total_count?.photo_count || 0 }, { title: t("generalSetting.live", { ns: "personal" }), number: countData?.counter?.total_count?.live_count || 0 }, { title: t("generalSetting.videoLength", { ns: "personal" }), number: formatDuration(countData?.counter?.total_count?.video_length || 0) }]}
/>
{/* 分类 */} {/* 分类 */}
<View style={{ height: 145 }}> <CarouselComponent data={userInfoDetails?.material_counter} />
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 16 }} >
{countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => {
return (
<CategoryComponent
key={index}
title={key}
data={[{ title: 'Video', number: value.video_count }, { title: 'Photo', number: value.photo_count }, { title: 'Length', number: formatDuration(value.video_length || 0) }]}
bgSvg={value.cover_url}
style={{ aspectRatio: 1, flex: 1 }}
/>
)
})}
</ScrollView>
</View>
{/* 作品数据 */} {/* 作品数据 */}
<View className='flex flex-row justify-between gap-[1rem]'> <View className='flex flex-row justify-between gap-[1rem]'>

View File

@ -0,0 +1,127 @@
import { Counter, UserCountData } from "@/types/user";
import * as React from "react";
import { Dimensions, StyleSheet, View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import Carousel, {
ICarouselInstance
} from "react-native-reanimated-carousel";
import { ThemedText } from "../ThemedText";
import { formatDuration } from "../utils/time";
import CategoryComponent from "./category";
interface Props {
data: Counter
}
interface CarouselData {
key: string,
value: UserCountData
}[]
const width = Dimensions.get("window").width;
function CarouselComponent(props: Props) {
const { data } = props;
const ref = React.useRef<ICarouselInstance>(null);
const progress = useSharedValue<number>(0);
const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]);
const dataHandle = () => {
const carouselData = { ...data?.category_count, total_count: data?.total_count }
// 1. 转换为数组并过滤掉 'total'
const entries = Object?.entries(carouselData)
?.filter(([key]) => key !== 'total_count')
?.map(([key, value]) => ({ key, value }));
// 2. 找到 total 数据
const totalEntry = {
key: 'total_count',
value: carouselData?.total_count
};
// 3. 插入到中间位置
const middleIndex = Math.floor((entries || [])?.length / 2);
entries?.splice(middleIndex, 0, totalEntry);
setCarouselDataValue(entries)
return entries;
}
const totleItem = (data: UserCountData) => {
return <View style={[styles.container]}>
{Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => (
<View style={styles.item} key={index}>
<ThemedText style={styles.title}>{item[0]}</ThemedText>
<ThemedText style={styles.number}>{item[1]}</ThemedText>
</View>
))}
</View>
}
React.useEffect(() => {
if (data) {
dataHandle()
}
}, [data]);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Carousel
ref={ref}
width={width * 0.8}
height={width * 0.8}
data={carouselDataValue || []}
mode="parallax"
onProgressChange={progress}
defaultIndex={carouselDataValue?.findIndex((item) => item?.key === 'total_count') - 1 || 0}
renderItem={({ item }) => {
if (item?.key === 'total_count') {
return totleItem(item.value)
}
return CategoryComponent(
{
title: item?.key,
data:
[
{ title: 'Video', number: item?.value?.video_count },
{ title: 'Photo', number: item?.value?.photo_count },
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
],
bgSvg: item?.value?.cover_url,
})
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#FFB645",
padding: 16,
borderRadius: 20,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
height: '100%',
},
item: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: 8
},
title: {
color: "#4C320C",
fontWeight: "700",
fontSize: 14,
},
number: {
color: "#fff",
fontWeight: "700",
fontSize: 32,
textAlign: 'right',
flex: 1,
paddingTop: 8
}
})
export default CarouselComponent;

View File

@ -14,8 +14,11 @@ const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => {
<View style={styles.backgroundContainer}> <View style={styles.backgroundContainer}>
<Image <Image
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/animals.png')} source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/animals.png')}
style={{ width: '100%', height: '100%' }} style={{
resizeMode="cover" width: "100%",
height: "100%",
resizeMode: "cover"
}}
/> />
<View style={styles.overlay} /> <View style={styles.overlay} />
</View> </View>
@ -37,11 +40,12 @@ const styles = StyleSheet.create({
borderRadius: 32, borderRadius: 32,
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',
aspectRatio: 1,
}, },
backgroundContainer: { backgroundContainer: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
width: '100%', width: "100%",
height: '100%', height: "100%",
}, },
overlay: { overlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,

13
package-lock.json generated
View File

@ -60,6 +60,7 @@
"react-native-picker-select": "^9.3.1", "react-native-picker-select": "^9.3.1",
"react-native-progress": "^5.0.1", "react-native-progress": "^5.0.1",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-reanimated-carousel": "^4.0.2",
"react-native-render-html": "^6.3.4", "react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
@ -14826,6 +14827,18 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-reanimated-carousel": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/react-native-reanimated-carousel/-/react-native-reanimated-carousel-4.0.2.tgz",
"integrity": "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.70.3",
"react-native-gesture-handler": ">=2.9.0",
"react-native-reanimated": ">=3.0.0"
}
},
"node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": { "node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz",

View File

@ -25,6 +25,7 @@
"expo-audio": "~0.4.8", "expo-audio": "~0.4.8",
"expo-background-task": "^0.2.8", "expo-background-task": "^0.2.8",
"expo-blur": "~14.1.5", "expo-blur": "~14.1.5",
"expo-clipboard": "~7.1.5",
"expo-constants": "~17.1.6", "expo-constants": "~17.1.6",
"expo-dev-client": "~5.2.4", "expo-dev-client": "~5.2.4",
"expo-device": "~7.1.4", "expo-device": "~7.1.4",
@ -33,6 +34,7 @@
"expo-haptics": "~14.1.4", "expo-haptics": "~14.1.4",
"expo-image-manipulator": "~13.1.7", "expo-image-manipulator": "~13.1.7",
"expo-image-picker": "~16.1.4", "expo-image-picker": "~16.1.4",
"expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-localization": "^16.1.5", "expo-localization": "^16.1.5",
"expo-location": "~18.1.5", "expo-location": "~18.1.5",
@ -64,6 +66,7 @@
"react-native-picker-select": "^9.3.1", "react-native-picker-select": "^9.3.1",
"react-native-progress": "^5.0.1", "react-native-progress": "^5.0.1",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-reanimated-carousel": "^4.0.2",
"react-native-render-html": "^6.3.4", "react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
@ -72,9 +75,7 @@
"react-native-uuid": "^2.0.3", "react-native-uuid": "^2.0.3",
"react-native-web": "~0.20.0", "react-native-web": "~0.20.0",
"react-native-webview": "13.13.5", "react-native-webview": "13.13.5",
"react-redux": "^9.2.0", "react-redux": "^9.2.0"
"expo-clipboard": "~7.1.5",
"expo-linear-gradient": "~14.1.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",

View File

@ -12,7 +12,7 @@ export interface User {
avatar_file_url?: string avatar_file_url?: string
} }
interface UserCountData { export interface UserCountData {
video_count: number, video_count: number,
photo_count: number, photo_count: number,
live_count: number, live_count: number,
@ -31,7 +31,7 @@ export interface CountData {
} }
} }
interface Counter { export interface Counter {
user_id: number, user_id: number,
total_count: UserCountData, total_count: UserCountData,
category_count: { category_count: {