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: {
position: 'absolute',
left: width / 2,
left: '50%',
top: -30,
marginLeft: -49,
transform: [{ translateX: -42.5 }],
width: 85,
height: 85,
justifyContent: 'center',
@ -162,7 +162,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
return (
<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}>
<TouchableOpacity
onPress={() => navigateTo('/memo-list')}

View File

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

View File

@ -6,16 +6,45 @@ import VideoTotalSvg from "@/assets/icons/svg/videoTotalWhite.svg";
import { BlurView } from "expo-blur";
import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
import { ThemedText } from "../ThemedText";
interface CategoryProps {
title: string;
data: { title: string, number: string | number }[];
data: { title: string, number: { s: number, m: number, h: number } | number }[];
bgSvg: string | null;
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 (
<View style={[styles.container, style]}>
<View style={[styles.container, style, { width: width * 0.7 }]}>
<View style={styles.backgroundContainer}>
<Image
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>
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
</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 style={styles.titleContent}>
@ -68,7 +109,7 @@ const styles = StyleSheet.create({
backdropFilter: 'blur(5px)',
},
content: {
padding: 16,
padding: 32,
justifyContent: "space-between",
flex: 1
},
@ -108,11 +149,11 @@ const styles = StyleSheet.create({
itemNumber: {
color: 'white',
fontSize: 28,
lineHeight: 30,
fontWeight: '700',
textAlign: 'left',
marginLeft: 8,
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 GradientText from '@/components/textLinear';
import { ThemedText } from '@/components/ThemedText';
import { useRouter } from 'expo-router';
import { useTranslation } from "react-i18next";
import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
@ -29,20 +27,20 @@ const MemberCard = ({ pro }: { pro: string }) => {
<IpSvg pro={pro} />
</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 />
<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
text={t("personal:member.unlock")}
width={width * 0.4}
fontSize={16}
lineHeight={1.5}
color={[
{ offset: "0%", color: "#FF512F" },
{ offset: "100%", color: "#F09819" }
{ offset: "0%", color: "#D0BFB0" },
{ offset: "32.89%", color: "#FFE57D" },
{ offset: "81.1%", color: "#FFFFFF" }
]}
/>
</View>

View File

@ -5,7 +5,7 @@ import { ThemedText } from '@/components/ThemedText';
import { UserInfoDetails } from '@/types/user';
import { useRouter } from 'expo-router';
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';
export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
@ -14,7 +14,7 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
const router = useRouter();
return (
<View className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'>
<View style={styles.container}>
{/* 头像 */}
<View className='w-auto'>
{userInfo?.user_info?.avatar_file_url && !imageError ? (
@ -30,45 +30,30 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
)}
</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 gap-2 w-full'>
<ThemedText
className='max-w-[80%] !text-textSecondary !font-semibold !text-2xl'
numberOfLines={1} // 限制为1行
ellipsizeMode="tail"
>
{userInfo?.user_info?.nickname}
</ThemedText>
<ScrollView
className='max-w-[20%]'
horizontal // 水平滚动
showsHorizontalScrollIndicator={false} // 隐藏滚动条
contentContainerStyle={{
flexDirection: 'row',
gap: 8, // 间距,
alignItems: 'center',
}}
>
<View className='flex flex-row items-center gap-2 w-full justify-between'>
<View style={{ width: "100%", flexDirection: "row", alignItems: "center", gap: 2 }}>
<ThemedText
style={{ maxWidth: "90%", color: '#4C320C', fontSize: 32, lineHeight: 36, fontWeight: '700' }}
numberOfLines={1}
ellipsizeMode="tail"
>
{userInfo?.user_info?.nickname}
</ThemedText>
{
userInfo?.medal_infos?.map((item, index) => (
userInfo?.membership_level && (
<Image
key={index}
source={{ uri: item.url }}
source={require('@/assets/images/png/owner/proIcon.png')}
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 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
style={{
color: '#AC7E35',
@ -84,23 +69,56 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
</ThemedText>
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
</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 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 >
);
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
width: "100%",
},
text: {
fontSize: 12,
fontWeight: '700',

View File

@ -3,9 +3,9 @@
* @param seconds
* @returns
*/
export function formatDuration(seconds: number): string {
export function formatDuration(seconds: number): { s: number, m: number, h: number } {
if (seconds < 60) {
return `${seconds}s`;
return { s: seconds, m: 0, h: 0 };
}
const minutes = Math.floor(seconds / 60);
@ -13,16 +13,16 @@ export function formatDuration(seconds: number): string {
if (minutes < 60) {
return remainingSeconds > 0
? `${minutes}min${remainingSeconds}s`
: `${minutes}min`;
? { s: remainingSeconds, m: minutes, h: 0 }
: { s: 0, m: minutes, h: 0 };
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
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": {
"goPremium": "Go Premium",
"unlock": "Unlock more memory magic"
"unlock": "解锁更多记忆魔法"
}
}