feat: 轮播图

This commit is contained in:
jinyaqiu 2025-08-07 15:25:07 +08:00
parent 399460a259
commit b428010a9c
9 changed files with 166 additions and 84 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -125,9 +125,9 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
}, },
centerButton: { centerButton: {
position: 'absolute', position: 'absolute',
left: width / 2, left: '50%',
top: -30, top: -30,
marginLeft: -49, transform: [{ translateX: -42.5 }],
width: 85, width: 85,
height: 85, height: 85,
justifyContent: 'center', justifyContent: 'center',
@ -162,7 +162,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: width }} /> <Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: "100%" }} />
<View style={styles.navContainer}> <View style={styles.navContainer}>
<TouchableOpacity <TouchableOpacity
onPress={() => navigateTo('/memo-list')} onPress={() => navigateTo('/memo-list')}

View File

@ -22,6 +22,7 @@ const width = Dimensions.get("window").width;
function CarouselComponent(props: Props) { function CarouselComponent(props: Props) {
const { data } = props; const { data } = props;
const [currentIndex, setCurrentIndex] = React.useState(0);
const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]); const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]);
const dataHandle = () => { const dataHandle = () => {
const carouselData = { ...data?.category_count, total_count: data?.total_count } const carouselData = { ...data?.category_count, total_count: data?.total_count }
@ -44,7 +45,7 @@ function CarouselComponent(props: Props) {
} }
const totleItem = (data: UserCountData) => { const totleItem = (data: UserCountData) => {
return <View style={styles.container}> return <View style={[styles.container, { width: width * 0.7 }]}>
{Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => ( {Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => (
<View style={styles.item} key={index}> <View style={styles.item} key={index}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, width: "75%", overflow: 'hidden' }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, width: "75%", overflow: 'hidden' }}>
@ -68,37 +69,43 @@ function CarouselComponent(props: Props) {
}, [data]); }, [data]);
return ( return (
<View style={{ flex: 1 }}> <View
style={{
flex: 1
}}>
<Carousel <Carousel
width={width} width={width}
height={width * 0.75} height={width * 0.75}
data={carouselDataValue || []} data={carouselDataValue || []}
mode="parallax" mode="parallax"
// defaultIndex={ onSnapToItem={(index) => setCurrentIndex(index)}
// carouselDataValue?.length defaultIndex={
// ? Math.max(0, Math.min( carouselDataValue?.length
// carouselDataValue.length - 1, ? Math.max(0, Math.min(
// carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1 carouselDataValue.length - 1,
// )) carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
// : 0 ))
// } : 0
}
modeConfig={{ modeConfig={{
parallaxScrollingScale: 1, parallaxScrollingScale: 1,
parallaxScrollingOffset: 150, parallaxScrollingOffset: 140,
parallaxAdjacentItemScale: 0.7 parallaxAdjacentItemScale: 0.7
}} }}
renderItem={({ item, index }) => { renderItem={({ item, index }) => {
const isActive = index === currentIndex;
const style: ViewStyle = { const style: ViewStyle = {
width: width, width: width,
height: width * 0.8, height: width * 0.8,
alignItems: "center", alignItems: "center",
// paddingTop: isActive && item?.key === 'total_count' ? 0 : 40
}; };
return ( return (
<View key={index} style={style}> <View key={index} style={[style]}>
{item?.key === 'total_count' ? ( {item?.key === 'total_count' ? (
totleItem(item.value) totleItem(item.value)
) : ( ) : (
<View style={{ flex: 1, width: width * 0.65 }}> <View>
{CategoryComponent({ {CategoryComponent({
title: item?.key, title: item?.key,
data: [ data: [
@ -107,6 +114,7 @@ function CarouselComponent(props: Props) {
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) } { title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
], ],
bgSvg: item?.value?.cover_url, bgSvg: item?.value?.cover_url,
width: width
})} })}
</View> </View>
)} )}
@ -124,13 +132,13 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 32, borderRadius: 32,
padding: 4 padding: 6
}, },
container: { container: {
backgroundColor: "#FFB645", backgroundColor: "#FFB645",
paddingVertical: 8, paddingVertical: 8,
paddingHorizontal: 16, paddingLeft: 32,
borderRadius: 16, borderRadius: 32,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
position: 'relative', position: 'relative',
@ -157,10 +165,10 @@ const styles = StyleSheet.create({
number: { number: {
color: "#fff", color: "#fff",
fontWeight: "700", fontWeight: "700",
fontSize: 26, fontSize: 28,
lineHeight: 30,
textAlign: 'left', textAlign: 'left',
flex: 1, flex: 1
paddingTop: 8
} }
}) })

View File

@ -6,16 +6,45 @@ import VideoTotalSvg from "@/assets/icons/svg/videoTotalWhite.svg";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native"; import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
import { ThemedText } from "../ThemedText"; import { ThemedText } from "../ThemedText";
interface CategoryProps { interface CategoryProps {
title: string; title: string;
data: { title: string, number: string | number }[]; data: { title: string, number: { s: number, m: number, h: number } | number }[];
bgSvg: string | null; bgSvg: string | null;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
width: number;
} }
const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => { const TimeUnit = ({ value, unit }: { value: number; unit: string }) => (
value > 0 && (
<>
<ThemedText style={styles.itemNumber}>{value}</ThemedText>
<ThemedText style={[styles.itemNumber, { fontSize: 10 }]}>{unit}</ThemedText>
</>
)
);
const CategoryComponent = ({ title, data, bgSvg, style, width }: CategoryProps) => {
const renderTimeDisplay = (time: { s: number; m: number; h: number }) => {
const { h, m, s } = time;
const showSeconds = s > 0 || (s === 0 && m === 0 && h === 0);
return (
<ThemedText style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>
<TimeUnit value={h} unit="h" />
<TimeUnit value={m} unit="m" />
{showSeconds && (
<>
<ThemedText style={styles.itemNumber}>{s}</ThemedText>
<ThemedText style={[styles.itemNumber, { fontSize: 10 }]}>s</ThemedText>
</>
)}
</ThemedText>
);
};
return ( return (
<View style={[styles.container, style]}> <View style={[styles.container, style, { width: width * 0.7 }]}>
<View style={styles.backgroundContainer}> <View style={styles.backgroundContainer}>
<Image <Image
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/people.png')} source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/people.png')}
@ -38,7 +67,19 @@ const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => {
</View> </View>
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText> <ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
</View> </View>
<ThemedText style={styles.itemNumber}>{item.number}</ThemedText> <View style={{ alignSelf: 'flex-start', flex: 1 }}>
{item?.title === "Length" ? (
typeof item.number === 'object' ? (
renderTimeDisplay(item.number)
) : (
<ThemedText style={[styles.itemNumber]}>{item.number}</ThemedText>
)
) : (
<ThemedText style={[styles.itemNumber]}>
{typeof item.number === 'number' ? item.number : 0}
</ThemedText>
)}
</View>
</View> </View>
))} ))}
<View style={styles.titleContent}> <View style={styles.titleContent}>
@ -68,7 +109,7 @@ const styles = StyleSheet.create({
backdropFilter: 'blur(5px)', backdropFilter: 'blur(5px)',
}, },
content: { content: {
padding: 16, padding: 32,
justifyContent: "space-between", justifyContent: "space-between",
flex: 1 flex: 1
}, },
@ -108,11 +149,11 @@ const styles = StyleSheet.create({
itemNumber: { itemNumber: {
color: 'white', color: 'white',
fontSize: 28, fontSize: 28,
lineHeight: 30,
fontWeight: '700', fontWeight: '700',
textAlign: 'left', textAlign: 'left',
marginLeft: 8, marginLeft: 8,
flex: 1, flex: 1,
paddingTop: 8
} }
}); });

View File

@ -1,7 +1,5 @@
import MemberBgSvg from '@/assets/icons/svg/memberBg.svg';
import ProTextSvg from '@/assets/icons/svg/proText.svg'; import ProTextSvg from '@/assets/icons/svg/proText.svg';
import GradientText from '@/components/textLinear'; import GradientText from '@/components/textLinear';
import { ThemedText } from '@/components/ThemedText';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native"; import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
@ -29,20 +27,20 @@ const MemberCard = ({ pro }: { pro: string }) => {
<IpSvg pro={pro} /> <IpSvg pro={pro} />
</View> </View>
{/* 会员标识 */} {/* 会员标识 */}
<View style={[styles.memberContainer, { left: width * 0.25, top: width * 0.1, opacity: 1 }]}> {/* <View style={[styles.memberContainer, { left: width * 0.25, top: width * 0.1, opacity: 1 }]}>
<MemberBgSvg /> <MemberBgSvg />
<ThemedText style={{ fontSize: 12, color: "#2D3D60", position: "absolute", left: 0, top: 0, bottom: 0, right: 0, textAlign: "center", textAlignVertical: "center" }}>{t("personal:member.goPremium")}</ThemedText> <ThemedText style={{ fontSize: 12, color: "#2D3D60", position: "absolute", left: 0, top: 0, bottom: 0, right: 0, textAlign: "center", textAlignVertical: "center" }}>{t("personal:member.goPremium")}</ThemedText>
</View> </View> */}
{/* 解锁更多魔法 */} {/* 解锁更多魔法 */}
<View style={{ position: "absolute", bottom: width * 0.02, left: -width * 0.01, opacity: pro === "pro" ? 1 : 0.5, width: width * 0.1, flexWrap: "wrap" }}> <View style={{ position: "absolute", bottom: width * 0.05, left: -width * 0.12, opacity: pro === "pro" ? 1 : 0.5, flexWrap: "wrap" }}>
<GradientText <GradientText
text={t("personal:member.unlock")} text={t("personal:member.unlock")}
width={width * 0.4}
fontSize={16} fontSize={16}
lineHeight={1.5} lineHeight={1.5}
color={[ color={[
{ offset: "0%", color: "#FF512F" }, { offset: "0%", color: "#D0BFB0" },
{ offset: "100%", color: "#F09819" } { offset: "32.89%", color: "#FFE57D" },
{ offset: "81.1%", color: "#FFFFFF" }
]} ]}
/> />
</View> </View>

View File

@ -5,7 +5,7 @@ import { ThemedText } from '@/components/ThemedText';
import { UserInfoDetails } from '@/types/user'; import { UserInfoDetails } from '@/types/user';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useState } from 'react'; import { useState } from 'react';
import { Image, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; import { Image, StyleSheet, TouchableOpacity, View } from 'react-native';
import CopyButton from '../copy'; import CopyButton from '../copy';
export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) { export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
@ -14,7 +14,7 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
const router = useRouter(); const router = useRouter();
return ( return (
<View className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'> <View style={styles.container}>
{/* 头像 */} {/* 头像 */}
<View className='w-auto'> <View className='w-auto'>
{userInfo?.user_info?.avatar_file_url && !imageError ? ( {userInfo?.user_info?.avatar_file_url && !imageError ? (
@ -30,45 +30,30 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
)} )}
</View> </View>
{/* 用户名 */} {/* 用户名 */}
<View className='flex flex-col w-[75%] gap-1'> <View className='flex flex-col w-[60%]'>
<View className='flex flex-row items-center justify-between w-full'> <View className='flex flex-row items-center justify-between w-full'>
<View className='flex flex-row items-center gap-2 w-full'> <View className='flex flex-row items-center gap-2 w-full justify-between'>
<ThemedText <View style={{ width: "100%", flexDirection: "row", alignItems: "center", gap: 2 }}>
className='max-w-[80%] !text-textSecondary !font-semibold !text-2xl' <ThemedText
numberOfLines={1} // 限制为1行 style={{ maxWidth: "90%", color: '#4C320C', fontSize: 32, lineHeight: 36, fontWeight: '700' }}
ellipsizeMode="tail" numberOfLines={1}
> ellipsizeMode="tail"
{userInfo?.user_info?.nickname} >
</ThemedText> {userInfo?.user_info?.nickname}
<ScrollView </ThemedText>
className='max-w-[20%]'
horizontal // 水平滚动
showsHorizontalScrollIndicator={false} // 隐藏滚动条
contentContainerStyle={{
flexDirection: 'row',
gap: 8, // 间距,
alignItems: 'center',
}}
>
{ {
userInfo?.medal_infos?.map((item, index) => ( userInfo?.membership_level && (
<Image <Image
key={index} source={require('@/assets/images/png/owner/proIcon.png')}
source={{ uri: item.url }}
style={{ width: 24, height: 24 }} style={{ width: 24, height: 24 }}
/> />
)) )
} }
</ScrollView>
<View className='flex flex-row items-center gap-2 border border-bgPrimary px-2 py-1 rounded-full'>
<StarSvg />
<ThemedText style={{ color: 'bgPrimary', fontSize: 14, fontWeight: '700' }}>{userInfo?.remain_points}</ThemedText>
</View> </View>
</View> </View>
</View> </View>
<View className='flex flex-row items-center justify-between w-full'> <View className='flex flex-row items-center justify-between w-full'>
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 2, maxWidth: '80%' }}> <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 2, maxWidth: '100%' }}>
<ThemedText <ThemedText
style={{ style={{
color: '#AC7E35', color: '#AC7E35',
@ -84,23 +69,56 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
</ThemedText> </ThemedText>
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} /> <CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
</View> </View>
<TouchableOpacity
onPress={() => {
router.push('/setting');
}}
activeOpacity={0.7}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.text}
>
<SettingSvg />
</TouchableOpacity>
</View> </View>
</View> </View>
<View style={{ flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderWidth: 1,
borderColor: '#FFB645',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 16,
}}>
<StarSvg />
<ThemedText
style={{
color: '#4C320C',
fontSize: 14,
fontWeight: '700',
maxWidth: 40,
lineHeight: 20
}}
numberOfLines={1}
ellipsizeMode="tail"
>
{userInfo?.remain_points}
</ThemedText>
</View>
<TouchableOpacity
onPress={() => {
router.push('/setting');
}}
activeOpacity={0.7}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.text}
>
<SettingSvg />
</TouchableOpacity>
</View>
</View > </View >
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
width: "100%",
},
text: { text: {
fontSize: 12, fontSize: 12,
fontWeight: '700', fontWeight: '700',

View File

@ -3,9 +3,9 @@
* @param seconds * @param seconds
* @returns * @returns
*/ */
export function formatDuration(seconds: number): string { export function formatDuration(seconds: number): { s: number, m: number, h: number } {
if (seconds < 60) { if (seconds < 60) {
return `${seconds}s`; return { s: seconds, m: 0, h: 0 };
} }
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
@ -13,16 +13,16 @@ export function formatDuration(seconds: number): string {
if (minutes < 60) { if (minutes < 60) {
return remainingSeconds > 0 return remainingSeconds > 0
? `${minutes}min${remainingSeconds}s` ? { s: remainingSeconds, m: minutes, h: 0 }
: `${minutes}min`; : { s: 0, m: minutes, h: 0 };
} }
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60; const remainingMinutes = minutes % 60;
if (remainingMinutes === 0) { if (remainingMinutes === 0) {
return `${hours}h`; return { s: 0, m: 0, h: hours };
} }
return `${hours}h${remainingMinutes}min`; return { s: seconds, m: minutes, h: hours };
} }

View File

@ -128,6 +128,6 @@
}, },
"member": { "member": {
"goPremium": "Go Premium", "goPremium": "Go Premium",
"unlock": "Unlock more memory magic" "unlock": "解锁更多记忆魔法"
} }
} }