feat:列表滚动加载

This commit is contained in:
weiwei committed 2025-10-24 18:24:07 +08:00
1 parent 2383f67f98
commit 568035dee7
6 files changed
+603 -291

No files matched your search

+7
View File
@@ -33,6 +33,13 @@
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
+230
View File
@@ -0,0 +1,230 @@
<template>
<uni-nav-bar dark :fixed="true" :border="false" :height="50" background-color="transparent" left-width="360rpx" right-width="500rpx">
<template v-slot:left>
<uni-icons type="back" size="30" color="#ffcd32" @click="back"></uni-icons>
<view class="nav-user-info" :style="navUserInfoStyle">
<image class="nav-avatar" src="" mode="scaleToFill" />
<text class="nav-name">稍后发非麻烦咖啡</text>
</view>
</template>
<template v-slot:right>
<view class="nav-bar-right">
<view class="status">365.56km · 在线</view>
<uni-icons type="more-filled" size="30" color="#ffcd32"></uni-icons>
</view>
</template>
</uni-nav-bar>
<scroll-view class="page" scroll-y @scroll="onScroll">
<view class="image" style="background-image: url(http://dummyimage.com/100x100/ac0ee3&text=其)">
<view class="info">
<image class="avatar" src="" />
<view class="name">{{ "啊发发" }}</view>
<view class="verification">真人认证</view>
<view class="tags">
<text class="tag">女·23岁</text>
<text class="tag">170cm</text>
</view>
<view class="introduction">{{ "发哈夫哈佛哈维合法i凤凰网你" }}</view>
</view>
<view class="filter" :style="filterStyle"></view>
</view>
<view class="album">
<view class="title">相册</view>
<image class="photo" v-for="(item, index) in 5" :key="index" src="http://dummyimage.com/100x100/ac0ee3&text=其" />
</view>
<view class="evaluate">
<view class="title">评价1+</view>
<view class="rate">
<view class="rate-item">
<text>身材</text>
<uni-rate :value="2" :touchable="false" :readonly="true" size="18" />
</view>
<view class="rate-item">
<text>颜值</text>
<uni-rate :value="2" :touchable="false" :readonly="true" size="18" />
</view>
<view class="rate-item">
<text>耐心</text>
<uni-rate :value="2" :touchable="false" :readonly="true" size="18" />
</view>
<view class="rate-item">
<text>有趣</text>
<uni-rate :value="2" :touchable="false" :readonly="true" size="18" />
</view>
</view>
</view>
<moment v-for="(item, index) in 100">{{ index }}</moment>
</scroll-view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import moment from "@/sheep/components/moment/moment.vue";
const scrollTop = ref(0);
const filterStyle = computed(() => {
let blur = 0;
const scrollY = scrollTop.value;
if (scrollY >= 0) {
blur = Math.min(1, scrollY / 300) * 10;
}
return {
backdropFilter: `blur(${blur}px)`,
};
});
const navUserInfoStyle = computed(() => {
let opacity = 1;
const scrollY = scrollTop.value;
if (scrollY >= 0) {
opacity = Math.min(1, scrollY / 300 < 0.4 ? 0 : scrollY / 300);
}
return {
opacity,
};
});
function back() {
uni.navigateBack();
}
function onScroll(e: any) {
console.log(e);
scrollTop.value = e.detail.scrollTop;
}
</script>
<style scoped lang="scss">
.nav-user-info {
display: flex;
align-items: center;
.nav-avatar {
width: 70rpx;
height: 70rpx;
background-color: #333;
border-radius: 50%;
margin-right: 10rpx;
}
.nav-name {
width: 140rpx;
@include no-wrap;
}
}
.nav-bar-right {
margin-right: 60rpx;
display: flex;
align-items: center;
.status {
font-size: $font-size-small;
padding: 10rpx 16rpx;
background-color: $color-theme-d;
border-radius: 36rpx;
margin-right: 40rpx;
}
}
.page {
height: calc(100vh - 100rpx);
background-color: $color-background;
overflow: hidden;
.image {
width: 100%;
height: 300px;
background-repeat: no-repeat;
transform-origin: top;
background-size: cover;
position: relative;
.info {
position: absolute;
bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
z-index: 100;
.avatar {
text-align: center;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #333;
margin-bottom: 20rpx;
}
.name {
font-size: $font-size-large;
margin-bottom: 20rpx;
}
.verification {
background-color: $color-theme;
font-size: $font-size-small;
color: black;
padding: 8rpx;
border-radius: 6rpx;
margin-bottom: 20rpx;
}
.tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 40rpx;
.tag {
background-color: $color-background;
font-size: $font-size-small;
color: $color-theme;
margin-right: 20rpx;
padding: 6rpx;
border-radius: 6rpx;
}
}
.introduction {
font-size: $font-size-medium;
}
}
.filter {
position: absolute;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(7, 17, 27, 0.2);
}
}
.album {
margin: 40rpx;
.title {
font-size: $font-size-medium-x;
margin: 20rpx 0;
}
.photo {
width: 140rpx;
height: 140rpx;
margin: 0 20rpx 20rpx 0;
border-radius: 10rpx;
}
}
.evaluate {
margin: 40rpx;
.title {
font-size: $font-size-medium-x;
margin: 20rpx 0;
}
.rate {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
font-size: $font-size-medium;
color: $color-theme;
.rate-item {
gap: 20rpx;
background-color: $color-background-d;
display: flex;
align-items: center;
padding: 12rpx 16rpx;
margin-bottom: 10rpx;
border-radius: 12rpx;
}
}
}
}
</style>
+208 -176
View File
@@ -1,20 +1,48 @@
<template>
<view class="home-page">
<view class="header">
<uni-nav-bar
dark
:fixed="true"
:border="false"
:height="50"
title=""
left-text=""
background-color="#222"
leftWidth="140rpx"
rightWidth="140rpx"
>
<view class="segmented">
<segmented v-model="current" :items="segmentedItems"></segmented>
</view>
<template v-slot:left>
<view class="location">
<uni-icons type="location" size="24" color="#ffcd32"></uni-icons>
<text>金华市</text>
</view>
<segmented class="segmented" v-model="current" :items="segmentedItems"></segmented>
</view>
</template>
<template v-slot:right></template>
</uni-nav-bar>
<scroll-view class="user-list" scroll-y>
<view class="user-card" v-for="user in userList" :key="user.id">
<image class="user-image" :src="user.image" />
<view class="home-page">
<scroll-view
class="user-list"
scroll-y
refresher-enabled
refresher-background="#222"
:refresher-triggered="refresherTriggered"
:refresher-threshold="100"
@refresherpulling="onRefresherPulling"
@refresherrefresh="onRefresherRefresh"
@refresherrestore="onRefresherRestore"
@refresherabort="onRefresherAbort"
@scrolltolower="onScrollToLower"
@scroll="onScroll"
>
<view class="user-card" v-for="user in userList" :key="user.id" @click="detail">
<image class="user-image" :src="user.avatar" />
<view class="user-info">
<view class="user">
<text class="user-name">{{ user.name }}</text>
<text class="user-status">{{ user.status }}</text>
<text class="user-status">{{ user.status || "离线" }}</text>
</view>
<view class="verification">
<text class="verify-badge">真人认证</text>
@@ -28,13 +56,17 @@
</view>
</view>
</view>
<!-- 加载更多组件 -->
<uni-load-more :status="loadMoreStatus" :content-text="loadMoreContent" @clickLoadMore="onClickLoadMore" />
<view style="height: 100rpx"></view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ref, reactive, onMounted } from "vue";
import segmented from "@/sheep/components/segmented/segmented.vue";
const current = ref(0);
@@ -52,175 +84,173 @@ interface User {
distance: string;
photoCount: number;
isVip: boolean;
status?: string;
}
const userList = ref<User[]>([
{
id: 1,
name: "晨晨",
avatar: "/static/avatar1.jpg",
gender: "女",
age: 25,
height: "168",
weight: "45",
occupation: "模特",
distance: "26.4",
photoCount: 2,
isVip: false,
status: "在线",
},
{
id: 2,
name: "艾伯特",
avatar: "/static/avatar2.jpg",
gender: "女",
age: 25,
height: "160",
weight: "45",
occupation: "学生",
distance: "32.5",
photoCount: 3,
isVip: false,
},
{
id: 3,
name: "粒儿云",
avatar: "/static/avatar3.jpg",
gender: "女",
age: 23,
height: "170",
weight: "50",
occupation: "老师",
distance: "64.6",
photoCount: 2,
isVip: false,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
{
id: 4,
name: "蓝天",
avatar: "/static/avatar4.jpg",
gender: "女",
age: 21,
height: "174",
weight: "49",
occupation: "老师",
distance: "64.5",
photoCount: 3,
isVip: true,
},
]);
// 数据状态管理
const userList = ref<User[]>([]);
const refresherTriggered = ref(false);
const loadMoreStatus = ref<"more" | "loading" | "noMore">("more");
const loadMoreContent = reactive({
contentdown: "上拉显示更多",
contentrefresh: "正在加载...",
contentnomore: "没有更多数据了",
});
// 分页参数
const pageInfo = reactive({
current: 1,
pageSize: 10,
total: 0,
hasMore: true,
});
// 模拟数据
const mockUsers: User[] = Array.from({ length: 70 }).map((_, index) => ({
id: index,
name: "山峰",
avatar: "/static/avatar2.jpg",
gender: "男",
age: 30,
height: "185",
weight: "75",
occupation: "健身教练",
distance: "5.2",
photoCount: 3,
isVip: false,
status: "在线",
}));
// 模拟API请求
const fetchUserList = async (page: number = 1, isRefresh: boolean = false) => {
try {
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 1000));
const startIndex = (page - 1) * pageInfo.pageSize;
const endIndex = startIndex + pageInfo.pageSize;
const newUsers = mockUsers.slice(startIndex, endIndex);
if (isRefresh) {
userList.value = newUsers;
pageInfo.current = 1;
} else {
userList.value.push(...newUsers);
}
pageInfo.current = page;
pageInfo.total = mockUsers.length;
pageInfo.hasMore = endIndex < mockUsers.length;
// 更新加载状态
if (pageInfo.hasMore) {
loadMoreStatus.value = "more";
} else {
loadMoreStatus.value = "noMore";
}
return true;
} catch (error) {
console.error("获取用户列表失败:", error);
uni.showToast({
title: "加载失败",
icon: "none",
});
return false;
}
};
// 下拉刷新事件
const onRefresherPulling = () => {
console.log("下拉刷新中...");
};
const onRefresherRefresh = async () => {
console.log("触发下拉刷新");
refresherTriggered.value = true;
const success = await fetchUserList(1, true);
refresherTriggered.value = false;
if (success) {
uni.showToast({
title: "刷新成功",
icon: "success",
});
}
};
const onRefresherRestore = () => {
console.log("下拉刷新复位");
};
const onRefresherAbort = () => {
console.log("下拉刷新中止");
refresherTriggered.value = false;
};
// 上拉加载更多
const onScrollToLower = async () => {
if (loadMoreStatus.value === "loading" || loadMoreStatus.value === "noMore") {
return;
}
console.log("触发上拉加载更多");
loadMoreStatus.value = "loading";
const success = await fetchUserList(pageInfo.current + 1, false);
if (!success) {
loadMoreStatus.value = "more";
}
};
// 点击加载更多
const onClickLoadMore = () => {
if (loadMoreStatus.value === "more") {
onScrollToLower();
}
};
// 滚动事件
const onScroll = (e: any) => {
// console.log('滚动事件:', e.detail);
};
// 用户详情
function detail() {
uni.navigateTo({
url: "/pages/detail/detail",
});
}
// 页面加载时获取数据
onMounted(() => {
fetchUserList(1, true);
});
</script>
<style scoped lang="scss">
.segmented {
margin: auto;
}
.location {
display: flex;
align-items: center;
font-size: $font-size-medium;
}
.home-page {
background-color: var(--color-background);
height: calc(100vh - 100rpx);
.header {
position: relative;
height: 100rpx;
.location {
position: absolute;
top: 0;
bottom: 0;
left: 10rpx;
display: flex;
align-items: center;
font-size: var(--font-size-medium);
}
.segmented {
position: absolute;
left: 0;
right: 0;
top: 50rpx;
transform: translateY(-50%);
}
}
background-color: $color-background;
height: calc(100vh - 100rpx - 100rpx);
.user-list {
height: calc(100vh - 100rpx - 100rpx);
height: 100%;
.user-card {
height: 300rpx;
display: flex;
justify-content: center;
align-items: center;
margin: 30rpx;
padding: 30rpx;
.user-image {
width: 200rpx;
height: 300rpx;
@@ -241,22 +271,24 @@ const userList = ref<User[]>([
justify-content: space-between;
margin-top: 16rpx;
.user-name {
font-size: var(--font-size-large);
font-size: $font-size-large;
font-weight: bold;
}
.user-status {
background-color: var(--color-theme-d);
font-size: var(--font-size-small);
background-color: $color-theme-d;
font-size: $font-size-small;
padding: 10rpx 20rpx;
border-radius: 36rpx;
color: white;
}
}
.verification {
margin-top: 16rpx;
.verify-badge {
background-color: var(--color-theme);
font-size: var(--font-size-small);
background-color: $color-theme;
font-size: $font-size-small;
color: black;
padding: 6rpx;
padding: 6rpx 12rpx;
border-radius: 6rpx;
}
}
@@ -265,11 +297,11 @@ const userList = ref<User[]>([
display: flex;
flex-wrap: wrap;
gap: 20rpx;
font-size: var(--font-size-small);
color: var(--color-text-ll);
font-size: $font-size-small;
color: $color-text-ll;
.tag {
background-color: var(--color-highlight-background);
padding: 10rpx;
background-color: $color-highlight-background;
padding: 10rpx 16rpx;
border-radius: 6rpx;
}
}
+13 -113
View File
@@ -13,39 +13,17 @@
<swiper-item>22</swiper-item>
<swiper-item>33</swiper-item>
</swiper>
<view class="moment-item" v-for="moment in momentsList" :key="moment.id">
<view class="user">
<image :src="moment.userAvatar" class="avatar" />
<view class="details">
<view>
<text class="username">{{ moment.username }}</text>
<text class="verification">{{ moment.verification }}</text>
</view>
<view class="info">女 · 20岁 | 学生</view>
</view>
</view>
<view class="content">
<text class="text">{{ moment.content }}</text>
<view class="images" v-if="moment.images && moment.images.length > 0">
<image v-for="(img, index) in moment.images" :key="index" :src="img" class="moment-image" />
</view>
</view>
<view class="address">
<uni-icons type="location" size="16" color="#B5B4BC"></uni-icons>
<text class="city">金华市 · 365.123km</text>
</view>
<view class="actions">
<view class="action-btn">
<uni-icons type="heart" size="24" color="#B5B4BC"></uni-icons>
<uni-icons type="heart-filled" size="24" color="#ffcd32"></uni-icons>
<text class="text">{{ moment.likes }} 喜欢</text>
</view>
<view class="action-btn">
<uni-icons type="chat" size="24" color="#B5B4BC"></uni-icons>
<text class="text">{{ moment.comments }} 打招呼</text>
</view>
</view>
</view>
<moment
v-for="item in momentsList"
:avatar="item.avatar"
:name="item.name"
:verification="item.verification"
:content="item.content"
:images="item.images"
:location="item.location"
:likes="item.likes"
:comments="item.comments"
/>
<view style="height: 100rpx"></view>
</scroll-view>
</view>
@@ -54,6 +32,7 @@
<script setup lang="ts">
import { ref } from "vue";
import segmented from "@/sheep/components/segmented/segmented.vue";
import moment from "@/sheep/components/moment/moment.vue";
const current = ref(0);
const segmentedItems = ["动态", "附近"];
@@ -69,7 +48,7 @@ interface Moment {
comments: number;
}
const momentsList = ref<Moment[]>([
const momentsList = ref<any[]>([
{
id: 1,
username: "小雨",
@@ -267,85 +246,6 @@ const momentsList = ref<Moment[]>([
}
.moments-list {
height: calc(100vh - 100rpx - 100rpx);
.moment-item {
padding: 20px;
border-bottom: 1px solid #333;
.user {
display: flex;
align-items: center;
margin-bottom: 15px;
.avatar {
width: 44px;
height: 44px;
border-radius: 20px;
margin-right: 10px;
background-color: #333;
}
.details {
flex: 1;
.username {
font-size: $font-size-medium-x;
margin-right: 20rpx;
}
.verification {
background-color: $color-theme;
font-size: $font-size-small;
color: black;
padding: 4rpx;
border-radius: 6rpx;
}
.info {
font-size: $font-size-small;
margin-top: 20rpx;
}
}
}
.content {
flex: 1;
padding: 0 20px;
margin-bottom: 15px;
.text {
line-height: 1.5;
}
.images {
margin-top: 20rpx;
display: flex;
gap: 10px;
flex-wrap: wrap;
.moment-image {
width: 100px;
height: 100px;
border-radius: 8px;
background-color: #333;
}
}
}
.address {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 20rpx;
margin-bottom: 20rpx;
.city {
font-size: $font-size-small;
}
}
.actions {
display: flex;
gap: 60rpx;
padding: 0 20rpx;
.action-btn {
display: flex;
align-items: center;
gap: 10rpx;
color: $color-text;
font-size: $font-size-medium;
.text {
font-size: $font-size-medium;
}
}
}
}
}
}
</style>
+1 -2
View File
@@ -113,8 +113,7 @@ const menuList = ref<MenuItem[]>([
border-radius: 40rpx;
padding: 8rpx 20rpx;
.text {
margin-left: 8rpx;
font-size: $font-size-small;
font-size: $font-size-medium;
color: #333;
}
}
+144
View File
@@ -0,0 +1,144 @@
<template>
<view class="moment">
<view class="user">
<image :src="avatar" class="avatar" />
<view class="details">
<view>
<text class="username">{{ name }}</text>
<text v-if="verification" class="verification">实人认证</text>
</view>
<view class="info">女 · 20岁 | 学生</view>
</view>
</view>
<view class="content">
<text class="text">{{ content }}</text>
<view class="images" v-if="images && images.length > 0">
<image v-for="(img, index) in images" :key="index" :src="img" class="moment-image" />
</view>
</view>
<view class="address">
<uni-icons type="location" size="16" color="#B5B4BC"></uni-icons>
<text class="city">{{ location }}</text>
</view>
<view class="actions">
<view class="action-btn">
<uni-icons type="heart" size="24" color="#B5B4BC"></uni-icons>
<uni-icons type="heart-filled" size="24" color="#ffcd32"></uni-icons>
<text class="text">{{ likes }} 喜欢</text>
</view>
<view class="action-btn">
<uni-icons type="chat" size="24" color="#B5B4BC"></uni-icons>
<text class="text">{{ comments }} 打招呼</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
withDefaults(
defineProps<
Partial<{
avatar: string;
name: string;
verification: boolean;
content: string;
images: string[];
location: string;
likes: number | string;
comments: number | string;
}>
>(),
{
avatar: "",
name: "",
verification: false,
content: "",
images: () => [],
location: "",
likes: 0,
comments: 0,
}
);
</script>
<style scoped lang="scss">
.moment {
padding: 20px;
border-bottom: 1px solid #333;
.user {
display: flex;
align-items: center;
margin-bottom: 15px;
.avatar {
width: 44px;
height: 44px;
border-radius: 20px;
margin-right: 10px;
background-color: #333;
}
.details {
flex: 1;
.username {
font-size: $font-size-medium-x;
margin-right: 20rpx;
}
.verification {
background-color: $color-theme;
font-size: $font-size-small;
color: black;
padding: 4rpx;
border-radius: 6rpx;
}
.info {
font-size: $font-size-small;
margin-top: 20rpx;
}
}
}
.content {
flex: 1;
padding: 0 20px;
margin-bottom: 15px;
.text {
line-height: 1.5;
}
.images {
margin-top: 20rpx;
display: flex;
gap: 10px;
flex-wrap: wrap;
.moment-image {
width: 100px;
height: 100px;
border-radius: 8px;
background-color: #333;
}
}
}
.address {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 20rpx;
margin-bottom: 20rpx;
.city {
font-size: $font-size-small;
}
}
.actions {
display: flex;
gap: 60rpx;
padding: 0 20rpx;
.action-btn {
display: flex;
align-items: center;
gap: 10rpx;
color: $color-text;
font-size: $font-size-medium;
.text {
font-size: $font-size-medium;
}
}
}
}
</style>