14 changed files with 1996 additions and 221 deletions
@ -0,0 +1 @@ |
|||
{"projectName":"trae_l237eu51"} |
@ -1,100 +1,111 @@ |
|||
<template> |
|||
<view> |
|||
<!-- 占位区域,防止内容被TabBar遮挡 --> |
|||
|
|||
<!-- 固定的TabBar --> |
|||
<view class="custom-tab-bar"> |
|||
<view class="tab-item" v-for="(item,i) in tabBarList" :key="i" @click="switchTab(i)"> |
|||
<text :style="{ 'color': currentTab === i?item.selectColor:'#fff' }">{{ item.text }}</text> |
|||
</view> |
|||
<view |
|||
class="tab-item" |
|||
v-for="(item, i) in tabBarList" |
|||
:key="i" |
|||
@click="switchTab(i)" |
|||
> |
|||
<text |
|||
:style="{ color: currentTab === i ? item.selectColor : '#fff' }" |
|||
>{{ item.text }}</text |
|||
> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
currentTab: { |
|||
type: Number, |
|||
default: 0 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
tabBarList: [ |
|||
{ |
|||
"pagePath": "pages/index/index", |
|||
"selectColor": "#00FF00", |
|||
"text": "首页" |
|||
}, |
|||
{ |
|||
"pagePath": "pages/index/readingBody", |
|||
"selectColor": "#00FF00", |
|||
"text": "阅读体" |
|||
}, |
|||
{ |
|||
"pagePath": "pages/index/sensoryStore", |
|||
"selectColor": "#00FF00", |
|||
"text": "有感商店" |
|||
}, |
|||
{ |
|||
"pagePath": "pages/index/intelligentAgent", |
|||
"selectColor": "#00FFFF", |
|||
"text": "智能体" |
|||
}, |
|||
{ |
|||
"pagePath": "pages/index/iSoul", |
|||
"selectColor": "#00FF00", |
|||
"text": "iSoul" |
|||
} |
|||
] |
|||
|
|||
}; |
|||
}, |
|||
onLoad() { |
|||
this.getCurrentTab(); |
|||
props: { |
|||
currentTab: { |
|||
type: Number, |
|||
default: 0, |
|||
}, |
|||
methods: { |
|||
getCurrentTab() { |
|||
const pages = getCurrentPages(); |
|||
const currentPage = pages[pages.length - 1]; |
|||
const currentPath = currentPage.route; |
|||
this.tabBarList.forEach((item, index) => { |
|||
if (item.pagePath === currentPath) { |
|||
this.currentTab = index; |
|||
} |
|||
}); |
|||
}, |
|||
data() { |
|||
return { |
|||
tabBarList: [ |
|||
{ |
|||
pagePath: "pages/index/index", |
|||
selectColor: "#00FF00", |
|||
text: "首页", |
|||
}, |
|||
{ |
|||
pagePath: "pages/index/readingBody", |
|||
selectColor: "#00FF00", |
|||
text: "阅读体", |
|||
}, |
|||
{ |
|||
pagePath: "pages/index/timeShopBank", |
|||
selectColor: "#00FF00", |
|||
text: "时间银行", |
|||
}, |
|||
{ |
|||
pagePath: "pages/index/intelligentAgent", |
|||
selectColor: "#00FFFF", |
|||
text: "智能体", |
|||
}, |
|||
switchTab(index) { |
|||
if (this.currentTab === index) return; |
|||
uni.switchTab({ |
|||
url: '/' + this.tabBarList[index].pagePath |
|||
}); |
|||
{ |
|||
pagePath: "pages/index/iSoul", |
|||
selectColor: "#00FF00", |
|||
text: "iSoul", |
|||
}, |
|||
], |
|||
}; |
|||
}, |
|||
onLoad() { |
|||
this.getCurrentTab(); |
|||
}, |
|||
methods: { |
|||
getCurrentTab() { |
|||
const pages = getCurrentPages(); |
|||
const currentPage = pages[pages.length - 1]; |
|||
const currentPath = currentPage.route; |
|||
this.tabBarList.forEach((item, index) => { |
|||
if (item.pagePath === currentPath) { |
|||
this.currentTab = index; |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
switchTab(index) { |
|||
if (this.currentTab === index) return; |
|||
uni.switchTab({ |
|||
url: "/" + this.tabBarList[index].pagePath, |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.custom-tab-bar { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
display: flex; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
height: 123rpx; |
|||
z-index: 30; |
|||
background: #989898; |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
display: flex; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
height: 123rpx; |
|||
z-index: 30; |
|||
background: #989898; |
|||
} |
|||
|
|||
.tab-item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
height: 100%; |
|||
} |
|||
|
|||
|
|||
.tab-item text { |
|||
font-size: 31rpx; |
|||
font-size: 31rpx; |
|||
} |
|||
</style> |
|||
</style> |
|||
|
@ -0,0 +1,502 @@ |
|||
<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> |
@ -0,0 +1,355 @@ |
|||
<template> |
|||
<view class="product-section"> |
|||
<!-- 商品标题区域 --> |
|||
<view class="title-section" :style="{ background: titleBgColor }"> |
|||
<div style="display: flex; align-items: center"> |
|||
<text class="title">{{ title }}</text> |
|||
<text class="more-btn" @click="handleMoreClick">更多</text> |
|||
</div> |
|||
</view> |
|||
|
|||
<!-- 商品列表 --> |
|||
<view class="product-list"> |
|||
<view |
|||
class="product-card" |
|||
v-for="(item, index) in productList" |
|||
:key="index" |
|||
@click="handleProductClick(item)" |
|||
> |
|||
<view class="card-image-container"> |
|||
<image |
|||
class="card-image" |
|||
:src="showImg(item.image)" |
|||
mode="aspectFill" |
|||
></image> |
|||
<!-- 图片蒙层 --> |
|||
<view class="image-overlay"></view> |
|||
<!-- 智能体标签 --> |
|||
<view class="content-box-info" v-if="!isFeel"> |
|||
<view class="ai-tag"> |
|||
<view class="ai-label" :style="{ borderColor: aiTagBorderColor, color: aiTagTextColor }">智能体</view> |
|||
<text class="ai-name">{{ item.aiName }}</text> |
|||
</view> |
|||
<!-- 头像 --> |
|||
<image |
|||
class="avatar" |
|||
:src="showImg(item.avatar)" |
|||
mode="aspectFill" |
|||
></image> |
|||
</view> |
|||
</view> |
|||
<view class="card-content"> |
|||
<view class="title-price-heart"> |
|||
<view class="card-title">{{ item.title }}</view> |
|||
<view class="card-price">¥{{ item.price }}</view> |
|||
<image |
|||
v-if="!item.isLiked" |
|||
class="heart-icon" |
|||
src="https://epic.js-dyyj.com/uploads/20250728/2f3ae212c01fa3b67be81abc5723cf5c.png" |
|||
@click.stop="handleLikeClick(item, index)" |
|||
></image> |
|||
<image |
|||
v-else |
|||
class="heart-icon" |
|||
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png" |
|||
@click.stop="handleLikeClick(item, index)" |
|||
></image> |
|||
<template v-if="isFeel"> |
|||
<image |
|||
v-if="!item.isShop" |
|||
class="shop-icon" |
|||
src="https://epic.js-dyyj.com/uploads/20250728/195bfc195a54b93c13595a01a5d8bb3b.png" |
|||
@click.stop="handleLikeClick(item, index)" |
|||
></image> |
|||
<image |
|||
v-else |
|||
class="shop-icon" |
|||
src="https://epic.js-dyyj.com/uploads/20250728/77c4546ac6415f9db69bb10888d2a975.png" |
|||
@click.isShop="handleLikeClick(item, index)" |
|||
></image> |
|||
</template> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "ProductSection", |
|||
props: { |
|||
// 标题 |
|||
title: { |
|||
type: String, |
|||
default: "商品列表", |
|||
}, |
|||
// 商品列表 |
|||
productList: { |
|||
type: Array, |
|||
default: () => [], |
|||
}, |
|||
// 更多按钮跳转路径 |
|||
moreUrl: { |
|||
type: String, |
|||
default: "", |
|||
}, |
|||
// 商品详情跳转路径前缀 |
|||
detailUrlPrefix: { |
|||
type: String, |
|||
default: "/subPackages/techan/detail", |
|||
}, |
|||
// 标题区域背景颜色 |
|||
titleBgColor: { |
|||
type: String, |
|||
default: "#9efffa", |
|||
}, |
|||
// AI标签边框颜色 |
|||
aiTagBorderColor: { |
|||
type: String, |
|||
default: "#01f1f196", |
|||
}, |
|||
// AI标签字体颜色 |
|||
aiTagTextColor: { |
|||
type: String, |
|||
default: "#02fcfc", |
|||
}, |
|||
isFeel:{ |
|||
type: Boolean, |
|||
default: false, |
|||
} |
|||
|
|||
}, |
|||
|
|||
methods: { |
|||
|
|||
// 处理更多按钮点击 |
|||
handleMoreClick() { |
|||
if (this.moreUrl) { |
|||
uni.navigateTo({ |
|||
url: this.moreUrl, |
|||
}); |
|||
} |
|||
this.$emit("more-click"); |
|||
}, |
|||
|
|||
// 处理商品点击 |
|||
handleProductClick(item) { |
|||
if (this.detailUrlPrefix) { |
|||
uni.navigateTo({ |
|||
url: `${this.detailUrlPrefix}?id=${item.id}`, |
|||
}); |
|||
} |
|||
this.$emit("product-click", item); |
|||
}, |
|||
|
|||
// 处理收藏点击 |
|||
handleLikeClick(item, index) { |
|||
// 更新本地状态 |
|||
const updatedItem = { ...item, isLiked: !item.isLiked }; |
|||
|
|||
// 显示提示 |
|||
uni.showToast({ |
|||
title: updatedItem.isLiked ? "已收藏" : "取消收藏", |
|||
icon: "none", |
|||
duration: 1500, |
|||
}); |
|||
|
|||
// 向父组件发送事件 |
|||
this.$emit("like-toggle", { item: updatedItem, index }); |
|||
}, |
|||
|
|||
// 显示图片 |
|||
showImg(img) { |
|||
if (!img) return; |
|||
|
|||
if (img.indexOf("https://") != -1 || img.indexOf("http://") != -1) { |
|||
return img; |
|||
} else { |
|||
return this.$options._base.prototype.NEWAPIURL + img; |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.product-section { |
|||
width: 100%; |
|||
} |
|||
|
|||
// 标题区域 |
|||
.title-section { |
|||
padding: 18rpx 24rpx; |
|||
margin: 40rpx 16rpx 0rpx; |
|||
display: inline-block; |
|||
border-radius: 14rpx 14rpx 0 0; |
|||
.title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #4d4d4d; |
|||
} |
|||
|
|||
.more-btn { |
|||
font-size: 18rpx; |
|||
color: #999999; |
|||
margin-left: 35rpx; |
|||
} |
|||
} |
|||
|
|||
// 商品列表 |
|||
.product-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 24rpx; |
|||
background-color: white; |
|||
padding: 22rpx; |
|||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); |
|||
} |
|||
|
|||
// 商品卡片 |
|||
.product-card { |
|||
background: #ffffff; |
|||
border-radius: 24rpx; |
|||
|
|||
overflow: hidden; |
|||
transition: transform 0.2s ease; |
|||
|
|||
&:active { |
|||
transform: scale(0.98); |
|||
} |
|||
} |
|||
|
|||
// 图片容器 |
|||
.card-image-container { |
|||
position: relative; |
|||
height: 200rpx; |
|||
overflow: hidden; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
.card-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
// 图片蒙层 |
|||
.image-overlay { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #00000047; |
|||
pointer-events: none; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
// 智能体标签 |
|||
.content-box-info { |
|||
position: absolute; |
|||
top: 0rpx; |
|||
left: 0rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
width: 666rpx; |
|||
padding: 0rpx 20rpx; |
|||
} |
|||
.ai-tag { |
|||
padding: 8rpx 0rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8rpx; |
|||
.ai-label { |
|||
border: 1rpx solid; |
|||
padding: 0rpx 15rpx; |
|||
height: 40rpx; |
|||
line-height: 38rpx; |
|||
font-weight: bold; |
|||
font-size: 16rpx; |
|||
border-radius: 4rpx; |
|||
} |
|||
|
|||
.ai-name { |
|||
font-size: 27rpx; |
|||
font-weight: bold; |
|||
color: #ffffff; |
|||
margin-left: 10rpx; |
|||
} |
|||
} |
|||
|
|||
// 头像 |
|||
.avatar { |
|||
width: 119rpx; |
|||
height: 119rpx; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
// 卡片内容 |
|||
.card-content { |
|||
padding: 24rpx 0; |
|||
} |
|||
|
|||
.title-price-heart { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
width: 100%; |
|||
} |
|||
|
|||
.card-title { |
|||
font-size: 27rpx; |
|||
font-weight: bold; |
|||
color: #000000; |
|||
line-height: 1.4; |
|||
flex: 1; |
|||
margin-right: 20rpx; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.card-price { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #ff6b6b; |
|||
margin-right: 24rpx; |
|||
flex-shrink: 0; |
|||
min-width: 120rpx; |
|||
} |
|||
|
|||
.heart-icon { |
|||
width: 35rpx; |
|||
height: 29rpx; |
|||
transition: all 0.3s ease; |
|||
flex-shrink: 0; |
|||
|
|||
&.liked { |
|||
opacity: 1; |
|||
filter: hue-rotate(320deg) saturate(2); |
|||
} |
|||
|
|||
&:active { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
.shop-icon{ |
|||
width: 39rpx; |
|||
height: 36rpx; |
|||
transition: all 0.3s ease; |
|||
flex-shrink: 0; |
|||
margin-left: 10rpx; |
|||
|
|||
&.liked { |
|||
opacity: 1; |
|||
filter: hue-rotate(320deg) saturate(2); |
|||
} |
|||
|
|||
&:active { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,352 @@ |
|||
<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> |
|||
<view class="like-info"> |
|||
<text class="like-icon">♥</text> |
|||
<text class="like-count">{{ item.likes }}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "WaterfallLayout", |
|||
props: { |
|||
// 数据源 |
|||
items: { |
|||
type: Array, |
|||
default: () => [], |
|||
}, |
|||
// 列数 |
|||
columnCount: { |
|||
type: Number, |
|||
default: 2, |
|||
}, |
|||
// 列间距(rpx) |
|||
columnGap: { |
|||
type: Number, |
|||
default: 16, |
|||
}, |
|||
// 项目间距(rpx) |
|||
itemGap: { |
|||
type: Number, |
|||
default: 16, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
positionedItems: [], |
|||
containerHeight: 0, |
|||
columnHeights: [], |
|||
}; |
|||
}, |
|||
watch: { |
|||
items: { |
|||
handler() { |
|||
this.$nextTick(() => { |
|||
this.calculateLayout(); |
|||
}); |
|||
}, |
|||
immediate: true, |
|||
deep: true, |
|||
}, |
|||
}, |
|||
mounted() { |
|||
this.calculateLayout(); |
|||
}, |
|||
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); |
|||
}); |
|||
|
|||
// 设置容器高度 |
|||
this.containerHeight = Math.max(...this.columnHeights) + this.itemGap; |
|||
}, |
|||
|
|||
// 计算单个项目位置 |
|||
calculateItemPosition(item, index) { |
|||
// 找到最短的列 |
|||
const shortestColumnIndex = this.columnHeights.indexOf(Math.min(...this.columnHeights)) |
|||
|
|||
// 协调的间距计算:左右边距20rpx,列间距16rpx |
|||
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) |
|||
// 修正top计算:第一行距离顶部itemGap,后续项目距离上一个项目itemGap |
|||
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 += 300; // 图片固定高度 |
|||
} |
|||
|
|||
// 内容区域padding |
|||
height += 32; // 上下padding各16rpx |
|||
|
|||
// 标题高度 - 更精确的计算 |
|||
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(稍微增加) + 底部间距 |
|||
} |
|||
|
|||
// 描述高度 - 更精确的计算 |
|||
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; // 用户信息高度 + 底部间距 |
|||
} |
|||
|
|||
return height; |
|||
}, |
|||
|
|||
// 清空所有项目 |
|||
clearItems() { |
|||
this.positionedItems = []; |
|||
this.containerHeight = 0; |
|||
this.columnHeights = new Array(this.columnCount).fill(0); |
|||
this.$emit("items-cleared"); |
|||
}, |
|||
|
|||
// 处理项目点击 |
|||
handleItemClick(item) { |
|||
this.$emit("item-click", item); |
|||
}, |
|||
|
|||
// 获取所有项目 |
|||
getAllItems() { |
|||
return this.positionedItems; |
|||
}, |
|||
|
|||
// 移除项目 |
|||
removeItem(itemId) { |
|||
const index = this.positionedItems.findIndex( |
|||
(item) => item.id === itemId |
|||
); |
|||
if (index !== -1) { |
|||
this.positionedItems.splice(index, 1); |
|||
this.calculateLayout(); // 重新计算布局 |
|||
} |
|||
this.$emit("item-removed", itemId); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.waterfall-layout { |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.waterfall-container { |
|||
position: relative; |
|||
width: 100%; |
|||
padding: 0 20rpx; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.waterfall-item { |
|||
box-sizing: border-box; |
|||
border-radius: 12rpx; |
|||
background: #fff; |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); |
|||
overflow: hidden; |
|||
transition: transform 0.2s ease; |
|||
} |
|||
|
|||
.waterfall-item:active { |
|||
transform: scale(0.98); |
|||
} |
|||
|
|||
.item-image { |
|||
width: 100%; |
|||
height: 300rpx; |
|||
object-fit: cover; |
|||
} |
|||
|
|||
.item-content { |
|||
padding: 16rpx; |
|||
} |
|||
|
|||
.item-title { |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
line-height: 1.3; |
|||
margin-bottom: 12rpx; |
|||
display: -webkit-box; |
|||
-webkit-box-orient: vertical; |
|||
-webkit-line-clamp: 2; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.item-desc { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
line-height: 1.4; |
|||
margin-bottom: 16rpx; |
|||
display: -webkit-box; |
|||
-webkit-box-orient: vertical; |
|||
-webkit-line-clamp: 2; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.item-tags { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 8rpx; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.tag { |
|||
padding: 4rpx 12rpx; |
|||
background: #f5f5f5; |
|||
color: #666; |
|||
font-size: 20rpx; |
|||
border-radius: 12rpx; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.item-footer { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-top: 16rpx; |
|||
} |
|||
|
|||
.user-info { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12rpx; |
|||
} |
|||
|
|||
.user-avatar { |
|||
width: 32rpx; |
|||
height: 32rpx; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.username { |
|||
font-size: 22rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.like-info { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6rpx; |
|||
} |
|||
|
|||
.like-icon { |
|||
font-size: 24rpx; |
|||
color: #ff6b6b; |
|||
} |
|||
|
|||
.like-count { |
|||
font-size: 22rpx; |
|||
color: #666; |
|||
} |
|||
</style> |
@ -1,56 +1,160 @@ |
|||
<template> |
|||
<view class="header" :style="{'height': height+'px','padding-top':statusBarHeight+'px'}"> |
|||
<image src="https://static.ticket.sz-trip.com/epicSoul/readingBody/search.png" mode="heightFix" |
|||
@click="gotoPath('/subPackages/search/search?type='+type)" v-if="isSearch"></image> |
|||
<view style="width: 50rpx;" v-else></view> |
|||
<image src="https://static.ticket.sz-trip.com/uploads/20250625/9bb05097e07570a934235983e1681a9f.png" mode="heightFix"></image> |
|||
<view style="width: 50rpx;"></view> |
|||
</view> |
|||
<view> |
|||
<!-- 占位区域,防止内容塌陷 --> |
|||
<view v-if="fixed" class="header-placeholder" :style="{ height: height + 'px' }"></view> |
|||
|
|||
<view |
|||
class="header" |
|||
:class="{ 'header-fixed': fixed }" |
|||
:style="{ height: height + 'px', 'padding-top': statusBarHeight + 'px' }" |
|||
> |
|||
<!-- 左侧:地区筛选和搜索 --> |
|||
<view class="left-section" v-if="isSearch"> |
|||
<view class="location-selector" @click="showLocationPicker"> |
|||
<text class="location-text">{{ selectedLocation }}</text> |
|||
<image |
|||
class="dropdown-icon" |
|||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEyIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xIDFMNiA2TDExIDEiIHN0cm9rZT0iIzk5OTk5OSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+" |
|||
mode="heightFix" |
|||
></image> |
|||
</view> |
|||
<image |
|||
class="search-icon" |
|||
src="https://static.ticket.sz-trip.com/epicSoul/readingBody/search.png" |
|||
mode="heightFix" |
|||
@click="gotoPath('/subPackages/search/search?type=' + type)" |
|||
></image> |
|||
</view> |
|||
<view class="left-section" v-else></view> |
|||
|
|||
<!-- 中间:Logo --> |
|||
<image |
|||
class="logo" |
|||
src="https://static.ticket.sz-trip.com/uploads/20250625/9bb05097e07570a934235983e1681a9f.png" |
|||
mode="heightFix" |
|||
></image> |
|||
|
|||
<!-- 右侧:占位 --> |
|||
<view class="right-section"></view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
isSearch: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
type: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
name:"header", |
|||
data() { |
|||
return { |
|||
// 导航栏参数 |
|||
height: 0, |
|||
statusBarHeight: 0, |
|||
}; |
|||
}, |
|||
mounted() { |
|||
this.initRectInfo() |
|||
}, |
|||
methods:{ |
|||
initRectInfo () { |
|||
const sysInfo = uni.getSystemInfoSync() |
|||
this.statusBarHeight = sysInfo.statusBarHeight |
|||
// 默认高度 |
|||
this.height = sysInfo.statusBarHeight + 40 |
|||
console.log("sysInfo" ,sysInfo) |
|||
}, |
|||
} |
|||
} |
|||
export default { |
|||
props: { |
|||
isSearch: { |
|||
type: Boolean, |
|||
default: true, |
|||
}, |
|||
type: { |
|||
type: String, |
|||
default: "", |
|||
}, |
|||
fixed: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}, |
|||
name: "header", |
|||
data() { |
|||
return { |
|||
// 导航栏参数 |
|||
height: 0, |
|||
statusBarHeight: 0, |
|||
// 地区选择 |
|||
selectedLocation: "苏州", |
|||
}; |
|||
}, |
|||
mounted() { |
|||
this.initRectInfo(); |
|||
}, |
|||
methods: { |
|||
initRectInfo() { |
|||
const sysInfo = uni.getSystemInfoSync(); |
|||
this.statusBarHeight = sysInfo.statusBarHeight; |
|||
// 默认高度 |
|||
this.height = sysInfo.statusBarHeight + 40; |
|||
console.log("sysInfo", sysInfo); |
|||
}, |
|||
showLocationPicker() { |
|||
// 显示地区选择器 |
|||
uni.showActionSheet({ |
|||
itemList: ["苏州", "上海", "杭州", "南京", "无锡"], |
|||
success: (res) => { |
|||
const locations = ["苏州", "上海", "杭州", "南京", "无锡"]; |
|||
this.selectedLocation = locations[res.tapIndex]; |
|||
// 触发地区变更事件 |
|||
this.$emit("locationChange", this.selectedLocation); |
|||
}, |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.header{ |
|||
flex-shrink: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
image{ |
|||
height: 46.16rpx; |
|||
} |
|||
} |
|||
</style> |
|||
.header { |
|||
flex-shrink: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 0 20rpx; |
|||
background-color: #fff; |
|||
|
|||
&.header-fixed { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
z-index: 999; |
|||
} |
|||
} |
|||
|
|||
.header-placeholder { |
|||
width: 100%; |
|||
} |
|||
|
|||
.left-section { |
|||
display: flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
|
|||
.location-selector { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 8rpx 16rpx; |
|||
// border: 2rpx solid #e0e0e0; |
|||
border-radius: 20rpx; |
|||
background-color: #fff; |
|||
min-width: 100rpx; |
|||
margin-right: 20rpx; |
|||
|
|||
.location-text { |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
.dropdown-icon { |
|||
width: 24rpx; |
|||
height: 16rpx; |
|||
} |
|||
} |
|||
|
|||
.search-icon { |
|||
height: 36.16rpx; |
|||
} |
|||
} |
|||
|
|||
.logo { |
|||
height: 36.16rpx; |
|||
position: absolute; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
} |
|||
|
|||
.right-section { |
|||
flex: 1; |
|||
} |
|||
</style> |
|||
|
@ -1,99 +1,237 @@ |
|||
<template> |
|||
<view class="bg"> |
|||
<headerVue :isSearch="false"></headerVue> |
|||
<view class="content"> |
|||
<swiper class="top-banner" :indicator-dots="false" :autoplay="false" v-if="topBanner && topBanner.length > 0"> |
|||
<swiper-item v-for="(item, index) in topBanner" :key="index" @click.stop="gotoUrlNew(item,index)"> |
|||
<image class="top-banner" :src="showImg(item.head_img)" mode="aspectFill" lazy-load="true"></image> |
|||
</swiper-item> |
|||
</swiper> |
|||
</view> |
|||
<CustomTabBar :currentTab="0" /> |
|||
<MusicControl /> |
|||
</view> |
|||
<view class="bg"> |
|||
<headerVue fixed></headerVue> |
|||
|
|||
<!-- 灵动岛组件 --> |
|||
<!-- 灵动岛组件 - 自包含,无需传递参数 --> |
|||
<DynamicIsland |
|||
ref="dynamicIsland" |
|||
@toggle="handleIslandToggle" |
|||
@action="handleIslandAction" |
|||
/> |
|||
|
|||
<view class="content" @click="handleContentClick"> |
|||
<!-- 权益商品区域 --> |
|||
<ProductSection |
|||
title="权益商品" |
|||
:productList="productList" |
|||
moreUrl="/pages/index/sensoryStore" |
|||
detailUrlPrefix="/subPackages/techan/detail" |
|||
@more-click="goToMore" |
|||
@product-click="goToDetail" |
|||
@like-toggle="handleLikeToggle" |
|||
/> |
|||
<ProductSection |
|||
titleBgColor="#92FF8F" |
|||
aiTagTextColor="#08FB05" |
|||
|
|||
aiTagBorderColor="#6EAA3D" |
|||
title="有感商品" |
|||
:productList="productListFeeling" |
|||
moreUrl="/pages/index/sensoryStore" |
|||
detailUrlPrefix="/subPackages/techan/detail" |
|||
@more-click="goToMore" |
|||
@product-click="goToDetail" |
|||
@like-toggle="handleLikeToggle" |
|||
isFeel |
|||
/> |
|||
<view class="tab-bar-placeholder"></view> |
|||
</view> |
|||
|
|||
<CustomTabBar :currentTab="0" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import headerVue from "@/components/header.vue" |
|||
import CustomTabBar from '@/components/CustomTabBar.vue'; |
|||
export default { |
|||
components: {CustomTabBar,headerVue,MusicControl}, |
|||
data() { |
|||
return { |
|||
topBanner: [] |
|||
} |
|||
}, |
|||
onLoad() { |
|||
|
|||
}, |
|||
onReady() { |
|||
this.getList() |
|||
}, |
|||
onShow() { |
|||
this.browse_record({type: 'page',title: '首页'}); |
|||
}, |
|||
onReachBottom() { |
|||
|
|||
}, |
|||
methods: { |
|||
gotoUrlNew(item,index) { |
|||
if(index == 0) { |
|||
uni.switchTab({ |
|||
url:"/pages/index/readingBody" |
|||
}) |
|||
}else if(index == 1) { |
|||
uni.switchTab({ |
|||
url:"/pages/index/intelligentAgent" |
|||
}) |
|||
} |
|||
}, |
|||
viewDetail(item) { |
|||
if (item.url) { |
|||
uni.navigateTo({ |
|||
url:"/subPackages/webPage/webPage?url="+encodeURIComponent(item.url) |
|||
}) |
|||
return |
|||
} |
|||
uni.navigateTo({ |
|||
url:'/subPackages/letter/detail?id='+item.id |
|||
}) |
|||
}, |
|||
getList() { |
|||
// 大轮播 |
|||
this.Post({ |
|||
type_id: 3, |
|||
position: 17, |
|||
}, '/api/adv/getAdv').then(res => { |
|||
if(res.data) { |
|||
this.topBanner = res.data; |
|||
} |
|||
}); |
|||
}, |
|||
gotoVideo(item) { |
|||
uni.navigateTo({ |
|||
url: '/subPackages/video/video?item=' + encodeURIComponent(JSON.stringify(item)) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
import MusicControl from "@/components/MusicControl.vue"; |
|||
import headerVue from "@/components/header.vue"; |
|||
import CustomTabBar from "@/components/CustomTabBar.vue"; |
|||
import DynamicIsland from "@/components/DynamicIsland.vue"; |
|||
import ProductSection from "@/components/ProductSection.vue"; |
|||
export default { |
|||
components: { |
|||
CustomTabBar, |
|||
headerVue, |
|||
MusicControl, |
|||
DynamicIsland, |
|||
ProductSection, |
|||
}, |
|||
computed: { |
|||
// 其他计算属性可以在这里添加 |
|||
}, |
|||
data() { |
|||
return { |
|||
topBanner: [], |
|||
productList: [ |
|||
{ |
|||
id: 1, |
|||
image: |
|||
"https://epic.js-dyyj.com/uploads/20250728/58aed304917c9d60761f833c4f8dceb8.png", |
|||
avatar: |
|||
"https://epic.js-dyyj.com/uploads/20250728/d27ef6e6c26877da7775664fed376c6f.png", |
|||
aiName: "文徵明", |
|||
title: "世界花园 | 研学之旅", |
|||
price: "588.00", |
|||
isLiked: true, |
|||
}, |
|||
{ |
|||
id: 2, |
|||
image: |
|||
"https://epic.js-dyyj.com/uploads/20250728/00e8704b23a0c9fd57023527146211b9.png", |
|||
avatar: |
|||
"https://epic.js-dyyj.com/uploads/20250728/d7bf0dd2f3f272afba687b525a7c575c.png", |
|||
aiName: "苏青壳", |
|||
title: "生命的扶持 | 风景之旅", |
|||
price: "398.00", |
|||
isLiked: false, |
|||
}, |
|||
], |
|||
productListFeeling: [ |
|||
{ |
|||
id: 1, |
|||
image: |
|||
"https://epic.js-dyyj.com/uploads/20250728/58aed304917c9d60761f833c4f8dceb8.png", |
|||
avatar: |
|||
"https://epic.js-dyyj.com/uploads/20250728/d27ef6e6c26877da7775664fed376c6f.png", |
|||
aiName: "文徵明", |
|||
title: "OUT OF SPACE 东方线香", |
|||
price: "588.00", |
|||
isLiked: true, |
|||
isShop:true |
|||
}, |
|||
{ |
|||
id: 2, |
|||
image: |
|||
"https://epic.js-dyyj.com/uploads/20250728/00e8704b23a0c9fd57023527146211b9.png", |
|||
avatar: |
|||
"https://epic.js-dyyj.com/uploads/20250728/d7bf0dd2f3f272afba687b525a7c575c.png", |
|||
aiName: "苏青壳", |
|||
title: "AI-Agent智能玩具", |
|||
price: "398.00", |
|||
isLiked: false, |
|||
}, |
|||
], |
|||
}; |
|||
}, |
|||
onLoad() {}, |
|||
onReady() { |
|||
this.getList(); |
|||
}, |
|||
onShow() { |
|||
this.browse_record({ type: "page", title: "首页" }); |
|||
}, |
|||
onPageScroll(e) { |
|||
// 通过全局事件通知灵动岛组件 |
|||
uni.$emit("pageScroll", e.scrollTop); |
|||
}, |
|||
onReachBottom() {}, |
|||
methods: { |
|||
gotoUrlNew(item, index) { |
|||
if (index == 0) { |
|||
uni.switchTab({ |
|||
url: "/pages/index/readingBody", |
|||
}); |
|||
} else if (index == 1) { |
|||
uni.switchTab({ |
|||
url: "/pages/index/intelligentAgent", |
|||
}); |
|||
} |
|||
}, |
|||
viewDetail(item) { |
|||
if (item.url) { |
|||
uni.navigateTo({ |
|||
url: |
|||
"/subPackages/webPage/webPage?url=" + encodeURIComponent(item.url), |
|||
}); |
|||
return; |
|||
} |
|||
uni.navigateTo({ |
|||
url: "/subPackages/letter/detail?id=" + item.id, |
|||
}); |
|||
}, |
|||
getList() { |
|||
// 大轮播 |
|||
this.Post( |
|||
{ |
|||
type_id: 3, |
|||
position: 17, |
|||
}, |
|||
"/api/adv/getAdv" |
|||
).then((res) => { |
|||
if (res.data) { |
|||
this.topBanner = res.data; |
|||
} |
|||
}); |
|||
}, |
|||
gotoVideo(item) { |
|||
uni.navigateTo({ |
|||
url: |
|||
"/subPackages/video/video?item=" + |
|||
encodeURIComponent(JSON.stringify(item)), |
|||
}); |
|||
}, |
|||
// 跳转到更多页面 |
|||
goToMore() { |
|||
uni.navigateTo({ |
|||
url: "/pages/index/sensoryStore", |
|||
}); |
|||
}, |
|||
// 跳转到商品详情 |
|||
goToDetail(item) { |
|||
uni.navigateTo({ |
|||
url: "/subPackages/techan/detail?id=" + item.id, |
|||
}); |
|||
}, |
|||
// 处理收藏状态切换 |
|||
handleLikeToggle({ item, index }) { |
|||
this.productList[index] = item; |
|||
}, |
|||
|
|||
// 处理灵动岛点击切换 |
|||
handleIslandToggle(isExpanded) { |
|||
console.log("灵动岛状态切换:", isExpanded ? "展开模式" : "紧凑模式"); |
|||
}, |
|||
|
|||
// 处理灵动岛操作按钮 |
|||
handleIslandAction() { |
|||
uni.showToast({ |
|||
title: "执行操作", |
|||
icon: "none", |
|||
}); |
|||
console.log("执行操作"); |
|||
}, |
|||
|
|||
// 处理内容区域点击 |
|||
handleContentClick() { |
|||
// 点击内容区域时收缩灵动岛 |
|||
if (this.$refs.dynamicIsland) { |
|||
this.$refs.dynamicIsland.collapseIsland(); |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.bg { |
|||
min-height: 100vh; |
|||
background: #f5f5f5; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
/* 页面样式 */ |
|||
|
|||
.content { |
|||
height: calc(100vh - 123rpx); |
|||
width: 100%; |
|||
padding: 0 20rpx; |
|||
box-sizing: border-box; |
|||
margin-top: 30rpx; |
|||
} |
|||
|
|||
.bg { |
|||
min-height: 100vh; |
|||
background: #FFFFFF; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
.content{ |
|||
height: calc(100vh - 123rpx); |
|||
width: 100%; |
|||
} |
|||
.top-banner { |
|||
width: 100%; |
|||
height: calc(100vh - 123rpx); |
|||
} |
|||
|
|||
.tab-bar-placeholder { |
|||
height: 143rpx; |
|||
width: 100%; |
|||
} |
|||
</style> |
|||
|
@ -0,0 +1,255 @@ |
|||
<template> |
|||
<view class="page-container"> |
|||
<!-- 导航栏组件 --> |
|||
<headerVue fixed></headerVue> |
|||
|
|||
<!-- 瀑布流组件 --> |
|||
<WaterfallLayout |
|||
:items="waterfallItems" |
|||
:column-count="2" |
|||
:column-gap="16" |
|||
:item-gap="16" |
|||
@item-click="handleItemClick" |
|||
style="margin-top: 20rpx" |
|||
/> |
|||
|
|||
<!-- 控制按钮 --> |
|||
<!-- <view class="controls"> |
|||
<button @click="addRandomItem" class="control-btn primary"> |
|||
添加项目 |
|||
</button> |
|||
<button @click="clearAllItems" class="control-btn danger">清空</button> |
|||
</view> --> |
|||
|
|||
<!-- 底部占位区域,防止内容被TabBar遮挡 --> |
|||
<view class="tab-bar-placeholder"></view> |
|||
|
|||
<!-- 底部TabBar组件 --> |
|||
<CustomTabBar :currentTab="2" /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import WaterfallLayout from "@/components/WaterfallLayout.vue"; |
|||
import headerVue from "@/components/header.vue"; |
|||
import CustomTabBar from "@/components/CustomTabBar.vue"; |
|||
|
|||
export default { |
|||
name: "TimeShopBank", |
|||
components: { |
|||
WaterfallLayout, |
|||
headerVue, |
|||
CustomTabBar, |
|||
}, |
|||
data() { |
|||
return { |
|||
waterfallItems: [], |
|||
autoAddEnabled: false, |
|||
// 图片URL数组 |
|||
images: [ |
|||
"https://images.unsplash.com/photo-1501854140801-50d01698950b?auto=format&fit=crop&w=800", |
|||
"https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=800", |
|||
"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=800", |
|||
"https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=800", |
|||
"https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?auto=format&fit=crop&w=800", |
|||
], |
|||
// 用户头像数组 |
|||
avatars: [ |
|||
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=100", |
|||
"https://images.unsplash.com/photo-1494790108755-2616b612b786?auto=format&fit=crop&w=100", |
|||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=100", |
|||
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=100", |
|||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=100", |
|||
], |
|||
// 用户名数组 |
|||
usernames: [ |
|||
"杨璐摄影", |
|||
"旅行者小王", |
|||
"风景猎人", |
|||
"自然探索者", |
|||
"摄影师阿明", |
|||
], |
|||
// 不同长度的标题 |
|||
shortTitles: ["城市风光", "自然美景", "湖光山色", "森林小径", "日出东方"], |
|||
mediumTitles: [ |
|||
"繁华都市的夜景与灯光", |
|||
"壮丽的山脉与蓝天白云", |
|||
"宁静的湖泊与秋日树木", |
|||
"蜿蜒的小路穿过金色森林", |
|||
"清晨第一缕阳光照亮大地", |
|||
], |
|||
longTitles: [ |
|||
"现代都市天际线在黄昏时分展现出迷人的轮廓,灯火辉煌的建筑倒映在水面上", |
|||
"雄伟的山脉在云雾缭绕中若隐若现,展现出大自然的壮丽与神秘", |
|||
"平静如镜的湖面倒映着五彩斑斓的秋叶,构成一幅完美的自然画卷", |
|||
"阳光透过茂密的树叶,在铺满落叶的林间小路上投下斑驳的光影", |
|||
"清晨的太阳从地平线缓缓升起,金色的光芒洒满整个大地,带来新的一天", |
|||
], |
|||
descriptions: [ |
|||
"探索城市中隐藏的美丽角落,发现不为人知的风景", |
|||
"大自然的鬼斧神工创造了无数令人惊叹的景观", |
|||
"水与山的完美结合,创造出宁静致远的意境", |
|||
"漫步在森林中,感受大自然的清新与宁静", |
|||
"日出时分的美景总能带给人们希望和力量", |
|||
], |
|||
tagsList: [ |
|||
["城市", "摄影", "旅行"], |
|||
["自然", "山脉", "探险"], |
|||
["湖泊", "秋天", "风景"], |
|||
["森林", "小路", "自然"], |
|||
["日出", "早晨", "美景"], |
|||
], |
|||
}; |
|||
}, |
|||
onLoad() { |
|||
this.initializeData(); |
|||
}, |
|||
|
|||
// 页面滚动到底部时触发 |
|||
onReachBottom() { |
|||
this.loadMoreItems(); |
|||
}, |
|||
methods: { |
|||
// 获取随机数据项 |
|||
getRandomItem() { |
|||
const titleType = |
|||
Math.random() > 0.5 |
|||
? Math.random() > 0.5 |
|||
? this.longTitles |
|||
: this.mediumTitles |
|||
: this.shortTitles; |
|||
|
|||
return { |
|||
id: Date.now() + Math.floor(Math.random() * 1000), |
|||
title: titleType[Math.floor(Math.random() * titleType.length)], |
|||
image: this.images[Math.floor(Math.random() * this.images.length)], |
|||
description: |
|||
this.descriptions[ |
|||
Math.floor(Math.random() * this.descriptions.length) |
|||
], |
|||
tags: this.tagsList[Math.floor(Math.random() * this.tagsList.length)], |
|||
// 新增用户信息 |
|||
user: { |
|||
avatar: this.avatars[Math.floor(Math.random() * this.avatars.length)], |
|||
name: this.usernames[ |
|||
Math.floor(Math.random() * this.usernames.length) |
|||
], |
|||
}, |
|||
likes: Math.floor(Math.random() * 999) + 1, // 1-999的随机点赞数 |
|||
}; |
|||
}, |
|||
|
|||
// 初始化数据 |
|||
initializeData() { |
|||
const initialItems = []; |
|||
for (let i = 0; i < 20; i++) { |
|||
initialItems.push(this.getRandomItem()); |
|||
} |
|||
this.waterfallItems = initialItems; |
|||
}, |
|||
|
|||
// 添加随机项目 |
|||
addRandomItem() { |
|||
const newItem = this.getRandomItem(); |
|||
this.waterfallItems.push(newItem); |
|||
}, |
|||
|
|||
// 清空所有项目 |
|||
clearAllItems() { |
|||
uni.showModal({ |
|||
title: "确认清空", |
|||
content: "确定要清空所有项目吗?", |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.waterfallItems = []; |
|||
} |
|||
}, |
|||
}); |
|||
}, |
|||
|
|||
// 处理项目点击 |
|||
handleItemClick(item) { |
|||
uni.showToast({ |
|||
title: `点击了: ${item.title}`, |
|||
icon: "none", |
|||
duration: 2000, |
|||
}); |
|||
}, |
|||
|
|||
// 处理项目添加 |
|||
handleItemAdded(item) { |
|||
console.log("项目已添加:", item.title); |
|||
}, |
|||
|
|||
// 处理自动添加请求 |
|||
handleAutoAddRequest() { |
|||
this.addRandomItem(); |
|||
}, |
|||
|
|||
// 加载更多项目(到达底部时调用) |
|||
loadMoreItems() { |
|||
const newItems = []; |
|||
for (let i = 0; i < 10; i++) { |
|||
newItems.push(this.getRandomItem()); |
|||
} |
|||
this.waterfallItems.push(...newItems); |
|||
|
|||
uni.showToast({ |
|||
title: "已加载10篇新文章", |
|||
icon: "success", |
|||
duration: 1500, |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.page-container { |
|||
min-height: 100vh; |
|||
background: #f8f8f8; |
|||
color: #333; |
|||
} |
|||
|
|||
.controls { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 20rpx; |
|||
justify-content: center; |
|||
margin: 40rpx 0; |
|||
padding: 0 20rpx; |
|||
} |
|||
|
|||
.control-btn { |
|||
padding: 24rpx 48rpx; |
|||
border-radius: 48rpx; |
|||
font-size: 28rpx; |
|||
border: none; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
background: white; |
|||
color: #333; |
|||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.control-btn.primary { |
|||
background: #ff4757; |
|||
color: white; |
|||
} |
|||
|
|||
.control-btn.danger { |
|||
background: #ff6b6b; |
|||
color: white; |
|||
} |
|||
|
|||
.control-btn:active { |
|||
transform: scale(0.95); |
|||
} |
|||
|
|||
.tab-bar-placeholder { |
|||
height: 120rpx; |
|||
} |
|||
|
|||
/* 自定义样式已移至WaterfallLayout组件内部 */ |
|||
</style> |
After Width: | Height: | Size: 319 B |
@ -0,0 +1,8 @@ |
|||
<template> |
|||
</template> |
|||
|
|||
<script> |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
Loading…
Reference in new issue