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

<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>