Browse Source

feat: 时间银行

dev_des
1054425342@qq.com 3 months ago
parent
commit
7c5e7c19df
  1. 182
      components/DynamicIsland.vue
  2. 271
      components/WaterfallLayout.vue

182
components/DynamicIsland.vue

@ -16,54 +16,75 @@
>
<!-- 展开状态 -->
<view v-if="!actualCompactState" class="expanded-content">
<template v-if="styleType!='timeShop'">
<view class="top-section" >
<text class="welcome-text">{{ title }}</text>
<view class="qr-code">
<image style="width: 39rpx;height: 39rpx;" src="https://epic.js-dyyj.com/uploads/20250728/88e0991e58e692c86c25e42537edc6ca.png"></image>
</view>
</view>
<view class="bottom-section">
<view class="stats-section">
<view class="stat-item">
<text class="stat-number">{{ getStatNumber("权益") }}</text>
<text class="stat-label">权益</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ getStatNumber("时间银行") }}</text>
<text class="stat-label">时间银行</text>
</view>
</view>
<view class="divider"></view>
<view class="action-section">
<text class="action-text">{{ actionText }}</text>
<image class="avatar" src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png" mode="aspectFill"></image>
</view>
</view>
</template>
<template v-if="styleType=='timeShop'">
<view class="bottom-section">
<view class="stats-section">
<view class="stat-item">
<text class="stat-number">{{ getStatNumber("时间银行") }}</text>
<text class="stat-label">时间银行</text>
</view>
</view>
<view class="divider"></view>
<view class="action-section">
<view class="action-text-box">
<view class="action-text-box-des">
在努力一点点为更好的未来蓄力吧
</view>
<view class="action-text-box-msg">
<image class="action-text-box-img" :src="showImg('/uploads/20250728/d7ac383902515c9b507c78fdc8d29520.png')"></image>
今日点赞和留言<text style="font-size: 30rpx;font-weight: bold;margin: 0 10rpx;">100</text>
</view>
</view>
<image class="avatar" src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png" mode="aspectFill"></image>
</view>
</view>
</template>
<template v-if="styleType != 'timeShop'">
<view class="top-section">
<text class="welcome-text">{{ title }}</text>
<view class="qr-code">
<image
style="width: 39rpx; height: 39rpx"
src="https://epic.js-dyyj.com/uploads/20250728/88e0991e58e692c86c25e42537edc6ca.png"
></image>
</view>
</view>
<view class="bottom-section">
<view class="stats-section">
<view class="stat-item">
<text class="stat-number">{{ getStatNumber("权益") }}</text>
<text class="stat-label">权益</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ getStatNumber("时间银行") }}</text>
<text class="stat-label">时间银行</text>
</view>
</view>
<view class="divider"></view>
<view class="action-section">
<text class="action-text">{{ actionText }}</text>
<image
class="avatar"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
mode="aspectFill"
></image>
</view>
</view>
</template>
<template v-if="styleType == 'timeShop'">
<view class="bottom-section">
<view class="stats-section">
<view class="stat-item">
<text class="stat-number">{{ getStatNumber("时间银行") }}</text>
<text class="stat-label">时间银行</text>
</view>
</view>
<view class="divider"></view>
<view class="action-section">
<view class="action-text-box">
<view class="action-text-box-des">
在努力一点点为更好的未来蓄力吧
</view>
<view class="action-text-box-msg">
<image
class="action-text-box-img"
:src="
showImg(
'/uploads/20250728/d7ac383902515c9b507c78fdc8d29520.png'
)
"
></image>
今日点赞和留言<text
style="font-size: 30rpx; font-weight: bold; margin: 0 10rpx"
>100</text
>
</view>
</view>
<image
class="avatar"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
mode="aspectFill"
></image>
</view>
</view>
</template>
</view>
<!-- 紧凑状态 -->
@ -71,7 +92,7 @@
<text class="compact-name">{{ getCompactName() }}</text>
<image
class="compact-avatar"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
mode="aspectFill"
></image>
</view>
@ -87,7 +108,7 @@ export default {
type: Boolean,
default: false,
},
styleType: {
styleType: {
type: String,
default: "",
},
@ -113,9 +134,8 @@ export default {
},
//
pageId: {
type: String,
default: 'default_page'
default: "default_page",
},
},
data() {
@ -143,7 +163,7 @@ export default {
// top
fixedTopPosition() {
// + (40px) + (20px)
return this.statusBarHeight + 40+20 ;
return this.statusBarHeight + 40 + 20;
},
// -
placeholderHeight() {
@ -209,7 +229,7 @@ export default {
addScrollListener() {
// ID
const eventName = `pageScroll_${this.pageId}`;
console.log('DynamicIsland 添加滚动监听:', eventName);
console.log("DynamicIsland 添加滚动监听:", eventName);
uni.$on(eventName, this.handlePageScroll);
},
//
@ -222,18 +242,9 @@ export default {
handlePageScroll(e) {
const scrollTop = e.scrollTop || e;
const shouldScroll = scrollTop > this.scrollThreshold;
console.log('DynamicIsland 接收到滚动事件:', {
pageId: this.pageId,
scrollTop,
shouldScroll,
currentIsScrolled: this.isScrolled
});
if (this.isScrolled !== shouldScroll) {
this.isScrolled = shouldScroll;
console.log('DynamicIsland 状态切换:', shouldScroll ? '紧凑模式' : '展开模式');
//
if (uni.vibrateShort) {
uni.vibrateShort();
@ -278,7 +289,11 @@ export default {
//
getUserInfo() {
try {
this.userInfo = (uni.getStorageSync('userInfo') && JSON.parse(uni.getStorageSync('userInfo'))) || this.$store.state.user.userInfo || {};
this.userInfo =
(uni.getStorageSync("userInfo") &&
JSON.parse(uni.getStorageSync("userInfo"))) ||
this.$store.state.user.userInfo ||
{};
//
if (this.userInfo && this.userInfo.nickname) {
this.currentTitle = `Hi!${this.userInfo.nickname},欢迎回来~`;
@ -321,8 +336,8 @@ export default {
-webkit-backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(255, 255, 255, 0.1);
border-radius: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3),
0 0 0 1rpx rgba(255, 255, 255, 0.05) inset;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3),
0 0 0 1rpx rgba(255, 255, 255, 0.05) inset;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
@ -395,7 +410,6 @@ export default {
.qr-code {
width: 32rpx;
height: 32rpx;
}
.qr-icon {
@ -563,22 +577,22 @@ export default {
// }
// }
// }
.action-text-box{
color: white;
.action-text-box-des{
font-size: 20rpx;
}
.action-text-box-msg{
font-size: 24rpx;
display: flex;
align-items: center;
margin-top: 20rpx;
}
.action-text-box-img{
width: 57rpx;
height: 46rpx;
margin-right: 10rpx;
}
.action-text-box {
color: white;
.action-text-box-des {
font-size: 20rpx;
}
.action-text-box-msg {
font-size: 24rpx;
display: flex;
align-items: center;
margin-top: 20rpx;
}
.action-text-box-img {
width: 57rpx;
height: 46rpx;
margin-right: 10rpx;
}
}
</style>

271
components/WaterfallLayout.vue

@ -1,49 +1,69 @@
<template>
<view class="waterfall-layout">
<!-- 瀑布流容器 -->
<view
class="waterfall-container"
:style="{ height: containerHeight + 'rpx' }"
>
<view
class="waterfall-item"
v-for="(item, index) in positionedItems"
:key="item.id"
:style="{
position: 'absolute',
left: item.left + 'rpx',
top: item.top + 'rpx',
width: item.width + 'rpx',
}"
@click="handleItemClick(item)"
>
<!-- 图片 -->
<image
v-if="item.image"
:src="item.image"
class="item-image"
mode="aspectFill"
/>
<!-- 内容区域 -->
<view class="item-content">
<text v-if="item.title" class="item-title">{{ item.title }}</text>
<!-- <text v-if="item.description" class="item-desc">{{ item.description }}</text> -->
<!-- <view v-if="item.tags && item.tags.length" class="item-tags">
<text class="tag" v-for="tag in item.tags" :key="tag">{{ tag }}</text>
</view> -->
<!-- 用户信息和点赞 -->
<view v-if="item.user" class="item-footer">
<view class="user-info">
<image
:src="item.user.avatar"
class="user-avatar"
mode="aspectFill"
/>
<text class="username">{{ item.user.name }}</text>
<view class="waterfall-container">
<!-- 左列 -->
<view class="column">
<view
v-for="(item, index) in leftItems"
:key="item.id || index"
class="waterfall-item"
@click="handleItemClick(item)"
>
<image
v-if="item.image"
:src="item.image"
class="item-image"
mode="aspectFill"
/>
<view class="item-content">
<text v-if="item.title" class="item-title">{{ item.title }}</text>
<view v-if="item.user" class="item-footer">
<view class="user-info">
<image
:src="item.user.avatar"
class="user-avatar"
mode="aspectFill"
/>
<text class="username">{{ item.user.name }}</text>
</view>
<view class="like-info">
<text class="like-icon"></text>
<text class="like-count">{{ item.likes }}</text>
</view>
</view>
<view class="like-info">
<text class="like-icon"></text>
<text class="like-count">{{ item.likes }}</text>
</view>
</view>
</view>
<!-- 右列 -->
<view class="column">
<view
v-for="(item, index) in rightItems"
:key="item.id || index"
class="waterfall-item"
@click="handleItemClick(item)"
>
<image
v-if="item.image"
:src="item.image"
class="item-image"
mode="aspectFill"
/>
<view class="item-content">
<text v-if="item.title" class="item-title">{{ item.title }}</text>
<view v-if="item.user" class="item-footer">
<view class="user-info">
<image
:src="item.user.avatar"
class="user-avatar"
mode="aspectFill"
/>
<text class="username">{{ item.user.name }}</text>
</view>
<view class="like-info">
<text class="like-icon"></text>
<text class="like-count">{{ item.likes }}</text>
</view>
</view>
</view>
</view>
@ -61,7 +81,7 @@ export default {
type: Array,
default: () => [],
},
//
// 2
columnCount: {
type: Number,
default: 2,
@ -79,128 +99,66 @@ export default {
},
data() {
return {
positionedItems: [],
containerHeight: 0,
columnHeights: [],
leftItems: [],
rightItems: [],
};
},
watch: {
items: {
handler() {
this.$nextTick(() => {
this.calculateLayout();
});
handler(newItems) {
this.calculateLayout(newItems);
},
immediate: true,
deep: true,
},
},
mounted() {
this.calculateLayout();
this.calculateLayout(this.items);
},
methods: {
//
calculateLayout() {
if (!this.items.length) {
this.positionedItems = [];
this.containerHeight = 0;
return;
}
//
this.columnHeights = new Array(this.columnCount).fill(0);
this.positionedItems = [];
//
this.items.forEach((item, index) => {
this.calculateItemPosition(item, index);
// DOM
getColumnHeight(columnRef) {
if (!columnRef) return 0;
const query = uni.createSelectorQuery().in(this);
return new Promise((resolve) => {
query.select(columnRef).boundingClientRect((data) => {
resolve(data ? data.height : 0);
}).exec();
});
//
this.containerHeight = Math.max(...this.columnHeights) + this.itemGap;
},
//
calculateItemPosition(item, index) {
//
const shortestColumnIndex = this.columnHeights.indexOf(Math.min(...this.columnHeights))
// 20rpx16rpx
const sideMargin = 20 //
const columnGap = 16 //
// 20rpx
const availableWidth = 750 - 2 * sideMargin // = 750 - 40 = 710rpx
const itemWidth = Math.floor((availableWidth - (this.columnCount - 1) * columnGap) / this.columnCount)
// - 20rpx
const left = sideMargin + shortestColumnIndex * (itemWidth + columnGap)
// topitemGapitemGap
const top = this.columnHeights[shortestColumnIndex] === 0 ? this.itemGap : this.columnHeights[shortestColumnIndex] + this.itemGap;
//
const estimatedHeight = this.estimateItemHeight(item);
//
this.positionedItems.push({
...item,
left,
top,
width: itemWidth,
});
//
this.columnHeights[shortestColumnIndex] = top + estimatedHeight;
},
//
estimateItemHeight(item) {
let height = 0;
//
if (item.image) {
height += 476; //
//
calculateLayout(items) {
if (!items || !items.length) {
this.leftItems = [];
this.rightItems = [];
return;
}
// padding
height += 32; // padding16rpx
//
this.leftItems = [];
this.rightItems = [];
// -
if (item.title) {
//
const titleLength = item.title.length;
const charsPerLine = 12; // 12
const titleLines = Math.min(Math.ceil(titleLength / charsPerLine), 2);
height += titleLines * 40 + 12; // 40rpx +
//
for (let i = 0; i < items.length; i++) {
this.addItem(items[i]);
}
},
// -
// if (item.description) {
// const descLength = item.description.length;
// const charsPerLine = 15; // 15
// const descLines = Math.min(Math.ceil(descLength / charsPerLine), 2);
// height += descLines * 36 + 16; // 36rpx +
// }
// -
// if (item.tags && item.tags.length) {
// //
// const tagRows = Math.ceil(item.tags.length / 3); // 3
// height += tagRows * 36 + 16; // 36rpx +
// }
//
if (item.user) {
height += 48 + 16; // +
//
addItem(item) {
//
if (this.leftItems.length <= this.rightItems.length) {
this.leftItems.push(item);
} else {
this.rightItems.push(item);
}
return height;
},
//
clearItems() {
this.positionedItems = [];
this.containerHeight = 0;
this.columnHeights = new Array(this.columnCount).fill(0);
this.leftItems = [];
this.rightItems = [];
this.$emit("items-cleared");
},
@ -211,19 +169,25 @@ export default {
//
getAllItems() {
return this.positionedItems;
return [...this.leftItems, ...this.rightItems];
},
//
removeItem(itemId) {
const index = this.positionedItems.findIndex(
(item) => item.id === itemId
);
//
let index = this.leftItems.findIndex(item => item.id === itemId);
if (index !== -1) {
this.positionedItems.splice(index, 1);
this.calculateLayout(); //
this.leftItems.splice(index, 1);
this.$emit("item-removed", itemId);
return;
}
//
index = this.rightItems.findIndex(item => item.id === itemId);
if (index !== -1) {
this.rightItems.splice(index, 1);
this.$emit("item-removed", itemId);
}
this.$emit("item-removed", itemId);
},
},
};
@ -236,12 +200,19 @@ export default {
}
.waterfall-container {
position: relative;
width: 100%;
display: flex;
gap: 16rpx;
padding: 0 20rpx;
box-sizing: border-box;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.waterfall-item {
box-sizing: border-box;
border-radius: 12rpx;

Loading…
Cancel
Save