feat: 轮播图
This commit is contained in:
parent
399460a259
commit
b428010a9c
17
assets/icons/svg/proIcon.svg
Normal file
17
assets/icons/svg/proIcon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 262 KiB |
BIN
assets/images/png/owner/proIcon.png
Normal file
BIN
assets/images/png/owner/proIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@ -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')}
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -128,6 +128,6 @@
|
||||
},
|
||||
"member": {
|
||||
"goPremium": "Go Premium",
|
||||
"unlock": "Unlock more memory magic"
|
||||
"unlock": "解锁更多记忆魔法"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user