You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
502 lines
11 KiB
502 lines
11 KiB
<template>
|
|
<!-- 灵动岛占位区域 - 始终存在但控制可见性 -->
|
|
<view
|
|
class="dynamic-island-placeholder"
|
|
:class="{ visible: isScrolled }"
|
|
:style="{ height: placeholderHeight + 'rpx' }"
|
|
>
|
|
<view
|
|
class="dynamic-island"
|
|
:class="{
|
|
compact: actualCompactState,
|
|
fixed: isFixed,
|
|
}"
|
|
:style="{ top: isFixed ? fixedTopPosition + 'px' : 0 }"
|
|
@click="handleToggle"
|
|
>
|
|
<!-- 展开状态 -->
|
|
<view v-if="!actualCompactState" class="expanded-content">
|
|
<view class="top-section">
|
|
<text class="welcome-text">{{ title }}</text>
|
|
<view class="qr-code">
|
|
<view class="qr-icon">
|
|
<image style="width: 39rpx;height: 39rpx;" src="https://epic.js-dyyj.com/uploads/20250728/ce88153acc92e0e2fca7acaa4ccec5c1.png"></image>
|
|
</view>
|
|
</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>
|
|
</view>
|
|
|
|
<!-- 紧凑状态 -->
|
|
<view v-else class="compact-content">
|
|
<text class="compact-name">{{ getCompactName() }}</text>
|
|
<image
|
|
class="compact-avatar"
|
|
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
|
|
mode="aspectFill"
|
|
></image>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: "DynamicIsland",
|
|
props: {
|
|
isCompact: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isFixed: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: "正在播放音乐",
|
|
},
|
|
subtitle: {
|
|
type: String,
|
|
default: "周杰伦 - 青花瓷",
|
|
},
|
|
avatarUrl: {
|
|
type: String,
|
|
default: "https://picsum.photos/80/80",
|
|
},
|
|
actionText: {
|
|
type: String,
|
|
default: "暂停",
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isExpanded: false,
|
|
statusBarHeight: 0,
|
|
isScrolled: false,
|
|
scrollThreshold: 160, // 灵动岛大卡片高度(160rpx)
|
|
// 内部数据,减少对外部props的依赖
|
|
currentTitle: "Hi!杰西卡酱,欢迎回来~",
|
|
currentSubtitle: "2个权益 | 120时间银行",
|
|
currentAvatar: "https://picsum.photos/80/80",
|
|
currentAction: "激活你的Agent",
|
|
};
|
|
},
|
|
computed: {
|
|
// 计算实际显示状态:只有在固定模式下才使用内部状态
|
|
actualCompactState() {
|
|
if (this.isScrolled) {
|
|
return !this.isExpanded;
|
|
}
|
|
return false; // 非滚动状态下始终展开
|
|
},
|
|
// 计算固定定位的top值
|
|
fixedTopPosition() {
|
|
// 状态栏高度 + 导航栏高度(40px) + 间距(20px)
|
|
return this.statusBarHeight + 40+20 ;
|
|
},
|
|
// 计算占位区域高度 - 始终保持固定高度,等于灵动岛大卡片高度
|
|
placeholderHeight() {
|
|
// 占位区域高度应该等于灵动岛展开状态的高度
|
|
// 包括上下边距:32rpx(上) + 160rpx(灵动岛) + 24rpx(下) = 216rpx
|
|
const islandHeightRpx = 160; // 灵动岛展开高度
|
|
const topMarginRpx = 32; // 上边距
|
|
const bottomMarginRpx = 24; // 下边距
|
|
return topMarginRpx + islandHeightRpx + bottomMarginRpx;
|
|
},
|
|
// 使用内部数据或props数据
|
|
title() {
|
|
return this.currentTitle;
|
|
},
|
|
subtitle() {
|
|
return this.currentSubtitle;
|
|
},
|
|
avatarUrl() {
|
|
return this.currentAvatar;
|
|
},
|
|
actionText() {
|
|
return this.currentAction;
|
|
},
|
|
// 计算是否固定
|
|
isFixed() {
|
|
return this.isScrolled;
|
|
},
|
|
},
|
|
mounted() {
|
|
// uni-app中通过父组件传递点击事件
|
|
this.setStatusBarHeight();
|
|
// 监听页面滚动
|
|
this.addScrollListener();
|
|
},
|
|
beforeDestroy() {
|
|
// 清理滚动监听
|
|
this.removeScrollListener();
|
|
},
|
|
methods: {
|
|
handleToggle() {
|
|
if (this.isScrolled) {
|
|
// 固定模式下切换内部状态
|
|
this.isExpanded = !this.isExpanded;
|
|
this.$emit("toggle", this.isExpanded);
|
|
} else {
|
|
// 非固定模式下触发外部事件
|
|
this.$emit("toggle");
|
|
}
|
|
},
|
|
handleAction() {
|
|
this.$emit("action");
|
|
},
|
|
// 外部调用收缩方法
|
|
collapseIsland() {
|
|
if (this.isScrolled && this.isExpanded) {
|
|
this.isExpanded = false;
|
|
this.$emit("toggle", this.isExpanded);
|
|
}
|
|
},
|
|
// 添加滚动监听
|
|
addScrollListener() {
|
|
// 监听全局页面滚动事件
|
|
uni.$on("pageScroll", this.handlePageScroll);
|
|
},
|
|
// 移除滚动监听
|
|
removeScrollListener() {
|
|
// 移除全局页面滚动监听
|
|
uni.$off("pageScroll", this.handlePageScroll);
|
|
},
|
|
// 处理页面滚动
|
|
handlePageScroll(e) {
|
|
const scrollTop = e.scrollTop || e;
|
|
const shouldScroll = scrollTop > this.scrollThreshold;
|
|
|
|
if (this.isScrolled !== shouldScroll) {
|
|
this.isScrolled = shouldScroll;
|
|
|
|
// 添加触觉反馈
|
|
if (uni.vibrateShort) {
|
|
uni.vibrateShort();
|
|
}
|
|
}
|
|
|
|
// 滚动时自动收缩展开的灵动岛
|
|
if (this.isScrolled) {
|
|
this.collapseIsland();
|
|
}
|
|
},
|
|
getCompactName() {
|
|
// 从标题中提取姓名
|
|
|
|
return "杰西卡酱";
|
|
},
|
|
getStatNumber(type) {
|
|
// 从副标题中解析统计数据
|
|
if (this.subtitle) {
|
|
if (type === "权益") {
|
|
const match = this.subtitle.match(/(\d+)个权益/);
|
|
return match ? match[1] : "0";
|
|
} else if (type === "时间银行") {
|
|
const match = this.subtitle.match(/(\d+)时间银行/);
|
|
return match ? match[1] : "0";
|
|
}
|
|
}
|
|
return "0";
|
|
},
|
|
// 设置状态栏高度
|
|
setStatusBarHeight() {
|
|
try {
|
|
const systemInfo = uni.getSystemInfoSync();
|
|
this.statusBarHeight = systemInfo.statusBarHeight || 0;
|
|
} catch (e) {
|
|
console.warn("获取系统信息失败:", e);
|
|
this.statusBarHeight = 0;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
/* 灵动岛占位区域样式 - 始终存在但控制可见性 */
|
|
.dynamic-island-placeholder {
|
|
width: 100%;
|
|
background: transparent;
|
|
position: relative;
|
|
opacity: 1;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.dynamic-island-placeholder.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* 当灵动岛不是固定状态时,确保它在占位符内正常显示 */
|
|
.dynamic-island-placeholder .dynamic-island:not(.fixed) {
|
|
position: relative;
|
|
z-index: 100;
|
|
}
|
|
|
|
.dynamic-island {
|
|
margin: 24rpx auto 24rpx;
|
|
z-index: 100;
|
|
|
|
background: #000000;
|
|
backdrop-filter: blur(20rpx);
|
|
border-radius: 40rpx;
|
|
|
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
overflow: hidden;
|
|
|
|
// 展开状态
|
|
width: 710rpx;
|
|
height: 220rpx;
|
|
|
|
// 紧凑状态
|
|
&.compact {
|
|
width: 300rpx;
|
|
height: 80rpx;
|
|
border-radius: 40rpx;
|
|
|
|
.expanded-content {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: opacity 0.2s ease-out, visibility 0s linear 0.2s;
|
|
}
|
|
}
|
|
|
|
&:not(.compact) {
|
|
.compact-content {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: opacity 0.2s ease-out, visibility 0s linear 0.2s;
|
|
}
|
|
}
|
|
|
|
// 固定定位状态
|
|
&.fixed {
|
|
position: fixed;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 998;
|
|
margin: 0;
|
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
animation: slideInFromTop 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
}
|
|
}
|
|
|
|
.expanded-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
padding: 24rpx 32rpx;
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transition: opacity 0.3s ease-in 0.4s, visibility 0s linear 0s;
|
|
}
|
|
|
|
.top-section {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.welcome-text {
|
|
font-size: 28rpx;
|
|
color: #ffffff;
|
|
font-weight: 500;
|
|
flex: 1;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.qr-code {
|
|
width: 32rpx;
|
|
height: 32rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.qr-icon {
|
|
width: 24rpx;
|
|
height: 24rpx;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-radius: 4rpx;
|
|
position: relative;
|
|
|
|
&::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2rpx;
|
|
left: 2rpx;
|
|
right: 2rpx;
|
|
bottom: 2rpx;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
border-radius: 2rpx;
|
|
}
|
|
}
|
|
|
|
.bottom-section {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.stats-section {
|
|
display: flex;
|
|
gap: 32rpx;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 32rpx;
|
|
color: #ffffff;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
margin-bottom: 4rpx;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 28rpx;
|
|
color: white;
|
|
line-height: 1;
|
|
font-weight: bold;
|
|
margin-top: 20rpx;
|
|
}
|
|
|
|
.divider {
|
|
width: 2rpx;
|
|
height: 60rpx;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
margin: 0 24rpx;
|
|
}
|
|
|
|
.action-section {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.action-text {
|
|
font-size: 26rpx;
|
|
color: #ffffff;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 200rpx;
|
|
}
|
|
|
|
.avatar {
|
|
width: 113rpx;
|
|
height: 113rpx;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
// 添加点击反馈动画
|
|
.dynamic-island:active {
|
|
transform: scale(0.98);
|
|
|
|
&.fixed {
|
|
transform: translateX(-50%) scale(0.98);
|
|
}
|
|
}
|
|
|
|
// 从顶部滑入动画
|
|
@keyframes slideInFromTop {
|
|
0% {
|
|
transform: translateX(-50%) translateY(-100%);
|
|
opacity: 0;
|
|
}
|
|
100% {
|
|
transform: translateX(-50%) translateY(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.compact-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 100%;
|
|
padding: 0 24rpx;
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transition: opacity 0.3s ease-in 0.4s, visibility 0s linear 0s;
|
|
}
|
|
|
|
.compact-name {
|
|
font-size: 27rpx;
|
|
color: #ffffff;
|
|
font-weight: bold;
|
|
flex: 1;
|
|
text-align: left;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 200rpx;
|
|
}
|
|
|
|
.compact-avatar {
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
border-radius: 50%;
|
|
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
// 动画定义
|
|
@keyframes pulse {
|
|
0% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.6;
|
|
transform: scale(1.1);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
// 响应式适配
|
|
// @media (max-width: 750rpx) {
|
|
// .dynamic-island {
|
|
// &.is-expanded {
|
|
// width: 100vw;
|
|
// max-width: 750rpx;
|
|
// }
|
|
|
|
// &.is-compact {
|
|
// width: 280rpx;
|
|
// }
|
|
// }
|
|
// }
|
|
</style>
|
|
|