Commit 940def5a authored by spc's avatar spc

Merge branch 'feature/fh_gaiban' into dev

parents c51a4f0e 93242848
<template>
<view class="scrollable-tabs">
<scroll-view
class="tabs-scroll"
scroll-x="true"
show-scrollbar="false"
:scroll-into-view="'tab-' + currentIndex"
:scroll-with-animation="true"
>
<view class="tabs-container">
<view
v-for="(item, index) in tabs"
:key="index"
:id="'tab-' + index"
class="tab-item"
:class="{ 'tab-active': currentIndex === index }"
@tap="handleTabClick(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
tabs: {
type: Array,
default: () => []
},
currentIndex: {
type: Number,
default: 0
}
})
const emit = defineEmits(['change'])
const handleTabClick = (index) => {
emit('change', index)
}
</script>
<style lang="less" scoped>
.scrollable-tabs {
width: 100%;
.tabs-scroll {
width: 100%;
white-space: nowrap;
.tabs-container {
display: flex;
align-items: center;
padding: 0 20rpx;
min-width: 100%;
.tab-item {
color: #333;
background-color: #e9edf1;
padding: 15rpx 20rpx;
font-size: 22rpx;
margin-right: 15rpx;
border-radius: 30rpx;
flex-shrink: 0;
white-space: nowrap;
transition: all 0.3s ease;
&:last-child {
margin-right: 0;
}
&.tab-active {
color: white;
background-color: #D3A358;
}
}
}
}
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.scrollable-tabs {
.tabs-scroll {
.tabs-container {
.tab-item {
padding: 12rpx 16rpx;
font-size: 20rpx;
margin-right: 12rpx;
}
}
}
}
}
</style>
\ No newline at end of file
# 可滚动标签实现指南
## 问题描述
当标签项(tabitem)总宽度超出父容器宽度时,需要实现左右滑动功能,让用户能够查看所有标签。
## 解决方案
### 方案一:修改现有代码(已实现)
#### 1. 修改模板结构
```vue
<scroll-view class="listbox" scroll-x="true" show-scrollbar="false" :scroll-into-view="'tab-' + channelTabIndex">
<view class="tab-container">
<view
v-for="(item, index) in productTabList"
:key="index"
:id="'tab-' + index"
:class="channelTabIndex === index ? 'tabitem tabActive' : 'tabitem'"
@tap="channelTabHandler(index, $event)"
>
{{ item }}
</view>
</view>
</scroll-view>
```
#### 2. 修改样式
```css
.listbox {
margin-top: 15rpx;
width: 686rpx;
white-space: nowrap;
.tab-container {
display: flex;
align-items: center;
padding: 0 20rpx;
min-width: 100%;
.tabitem {
color: @color-black-deep;
background-color: #e9edf1;
padding: 15rpx 20rpx;
font-size: 22rpx;
margin-right: 15rpx;
border-radius: 30rpx;
flex-shrink: 0; /* 防止压缩 */
white-space: nowrap; /* 防止换行 */
&:last-child {
margin-right: 0;
}
}
.tabActive {
color: white;
background-color: @color-gold-main;
}
}
}
```
### 方案二:使用可复用组件(推荐)
#### 1. 使用 ScrollableTabs 组件
```vue
<template>
<view class="productcontai">
<text class="maintitle">{{ erqiPeizhi.title1 }}</text>
<ScrollableTabs
:tabs="productTabList"
:currentIndex="channelTabIndex"
@change="handleTabChange"
/>
</view>
</template>
<script setup>
import ScrollableTabs from '../components/ScrollableTabs.vue'
const handleTabChange = (index) => {
channelTabIndex.value = index
}
</script>
```
## 关键属性说明
### 1. scroll-view 属性
```vue
<scroll-view
scroll-x="true" <!-- 启用横向滚动 -->
show-scrollbar="false" <!-- 隐藏滚动条 -->
:scroll-into-view="'tab-' + currentIndex" <!-- 自动滚动到指定元素 -->
:scroll-with-animation="true" <!-- 启用滚动动画 -->
>
```
### 2. 样式关键属性
```css
/* 容器样式 */
.listbox {
white-space: nowrap; /* 防止换行 */
}
.tab-container {
display: flex; /* 弹性布局 */
min-width: 100%; /* 最小宽度 */
padding: 0 20rpx; /* 左右内边距 */
}
.tabitem {
flex-shrink: 0; /* 防止压缩 */
white-space: nowrap; /* 防止换行 */
margin-right: 15rpx; /* 右边距 */
}
```
## 实现效果
### 功能特性
- ✅ 支持横向滚动
- ✅ 自动滚动到选中项
- ✅ 隐藏滚动条
- ✅ 平滑滚动动画
- ✅ 响应式设计
- ✅ 防止标签压缩
### 用户体验
- 当标签总宽度超出容器时,可以左右滑动查看
- 点击标签时自动滚动到可见区域
- 滚动过程平滑自然
- 适配不同屏幕尺寸
## 最佳实践
### 1. 标签间距设置
```css
.tabitem {
margin-right: 15rpx; /* 标签间距 */
&:last-child {
margin-right: 0; /* 最后一个标签无右边距 */
}
}
```
### 2. 容器内边距
```css
.tab-container {
padding: 0 20rpx; /* 左右留出空间 */
}
```
### 3. 防止压缩
```css
.tabitem {
flex-shrink: 0; /* 防止标签被压缩 */
white-space: nowrap; /* 防止文字换行 */
}
```
### 4. 自动滚动
```vue
:scroll-into-view="'tab-' + currentIndex"
```
## 常见问题解决
### 1. 滚动不生效
- 检查 `scroll-x="true"` 是否设置
- 确认容器宽度小于内容宽度
- 检查 `flex-shrink: 0` 是否设置
### 2. 标签被压缩
- 添加 `flex-shrink: 0`
- 设置 `white-space: nowrap`
### 3. 滚动条显示
- 设置 `show-scrollbar="false"`
### 4. 自动滚动不工作
- 确认 `scroll-into-view` 的 ID 正确
- 检查元素 ID 是否唯一
## 响应式设计
### 移动端优化
```css
@media screen and (max-width: 750rpx) {
.tabitem {
padding: 12rpx 16rpx; /* 减小内边距 */
font-size: 20rpx; /* 减小字体 */
margin-right: 12rpx; /* 减小间距 */
}
}
```
### 平板端优化
```css
@media screen and (min-width: 751rpx) and (max-width: 1024rpx) {
.tab-container {
padding: 0 30rpx; /* 增加内边距 */
}
}
```
## 性能优化
### 1. 使用 transform 动画
```css
.tabitem {
transition: all 0.3s ease;
}
```
### 2. 避免频繁重绘
```css
.tab-container {
will-change: transform; /* 优化滚动性能 */
}
```
### 3. 合理设置滚动阈值
```vue
<scroll-view
:scroll-into-view="shouldScroll ? 'tab-' + currentIndex : ''"
>
```
## 总结
通过以上实现,标签列表现在支持:
1. **横向滚动**:当标签总宽度超出容器时自动启用
2. **自动定位**:点击标签时自动滚动到可见区域
3. **平滑动画**:滚动过程自然流畅
4. **响应式设计**:适配不同屏幕尺寸
5. **性能优化**:避免不必要的重绘和计算
这种实现方式既保持了原有功能,又提供了更好的用户体验,特别适合标签数量较多或标签文字较长的场景。
\ No newline at end of file
This diff is collapsed.
......@@ -80,6 +80,19 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/expertTeamPage/expertTeamPage",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/syWebview/syWebview",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/feedingRecord/feedingRecord",
"style": {
......
<template>
<view class="expertTeamPage">
<image :class="['bg', 'bg_1']" v-show="curTabIndex==1"
:src="$baseUrl + 'homepage/et_bg_1.jpg'"
mode="aspectFit">
</image>
<image :class="['bg', 'bg_2']" v-show="curTabIndex==2"
:src="$baseUrl + 'homepage/et_bg_2.jpg'"
mode="aspectFit">
</image>
<image :class="['bg', 'bg_3']" v-show="curTabIndex==3"
:src="$baseUrl + 'homepage/et_bg_3.jpg'"
mode="aspectFit">
</image>
<view class="tab_box">
<view class="tab tab_1" @tap="onSelTab(1)"></view>
<view class="tab tab_2" @tap="onSelTab(2)"></view>
<view class="tab tab_3" @tap="onSelTab(3)"></view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app'
const curTabIndex = ref(1);
onLoad((options) => {
curTabIndex.value = options.tab
console.log('接收到的参数:', curTabIndex.value)
});
const onSelTab = (index) => {
curTabIndex.value = index;
}
</script>
<style lang="less" scoped>
.expertTeamPage {
position: absolute;
width: 100vw;
height: 100vh;
overflow-y: scroll;
.bg {
position: absolute;
width: 100vw;
height: auto;
background-size: 100% 100%;
}
.bg_1 {
height: 1476rpx;
}
.bg_2 {
height: 6108rpx;
}
.bg_3 {
height: 2503rpx;
}
.tab_box{
position: absolute;
top: 505rpx;
z-index: 100;
width: 100%;
height: 100rpx;
display: flex;
justify-content: center;
gap: 0rpx;
opacity: 0;
.tab {
// position: absolute;
width: 230rpx;
height: 60rpx;
}
.tab_1{
// left: 45rpx;
background-color: red;
}
.tab_2{
// left: 290rpx;
background-color: #00ff00;
}
.tab_3{
// left: 510rpx;
background-color: #0f0f0f;
}
}
}
</style>
\ No newline at end of file
<!--
Webview页面使用说明:
1. 基本使用:
uni.navigateTo({
url: '/pages/syWebview/syWebview?url=' + encodeURIComponent('https://example.com')
});
2. 带标题的使用:
uni.navigateTo({
url: '/pages/syWebview/syWebview?url=' + encodeURIComponent('https://example.com') + '&title=' + encodeURIComponent('页面标题')
});
3. 分享功能:
- 页面会自动接收webview发送的消息
- 消息格式:{ title: '标题', link: '链接', imgUrl: '图片', desc: '描述' }
- 支持数组格式:[{ title: '标题', link: '链接', imgUrl: '图片', desc: '描述' }]
4. 分享数据示例:
{
title: '分享标题',
link: 'https://example.com/share',
imgUrl: 'https://example.com/image.jpg',
desc: '分享描述'
}
-->
<template>
<view class="webview-container">
<!-- Webview内容 -->
<web-view :src="webviewUrl" @message="getMessage" @load="onWebviewLoad" @error="onWebviewError"
class="webview-content"></web-view>
</view>
</template>
<script>
import { useUserStore } from '@/stores/user.js';
import { useGlobalStore } from '../../stores/global';
export default {
data() {
return {
statusBarHeight: 0,
webviewUrl: '',
pageTitle: '加载中...',
share: null, // 分享数据
pageOptions: {} // 页面参数
}
},
async onLoad(options) {
// 获取状态栏高度
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// 确保登录以获取 cuk(异步回调,可能未立即生效)
await this.wxAutoLogin();
// 固定写死的 URL(不从 options 读取 baseUrl)
const baseUrl = 'https://25niansuyuan.feihe.com/projects/Firmus/dev/index';
const store = useGlobalStore();
const cuk = store.cuk || uni.getStorageSync('cuk');
// 可选:设置页面标题(不从 params 移除,仍然透传到 H5)
if (options && options.title) {
try { this.pageTitle = decodeURIComponent(options.title); } catch (_) { this.pageTitle = options.title; }
}
// 将 options 的所有参数原样作为查询参数传给 H5,并补齐 cuk
const params = { ...(options || {}) };
if (cuk && !params.cuk) params.cuk = cuk;
const paramStr = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
this.webviewUrl = paramStr
? baseUrl + (baseUrl.includes('?') ? '&' : '?') + paramStr
: baseUrl;
console.log('Webview页面加载,URL:', this.webviewUrl);
},
onShareAppMessage(options) {
// 分享功能
const { share } = this;
console.warn(share?.link, "share.link--------", JSON.stringify(options));
if (share) {
const shareurl = share.link;
return {
title: share.title,
path: shareurl,
imageUrl: share.imgUrl || '',
success: function (res) {
console.log(res, '分享成功');
uni.showToast({
title: '分享成功',
icon: 'success'
});
},
fail: function (res) {
console.log(res, '分享失败');
uni.showToast({
title: '分享失败',
icon: 'none'
});
},
complete: function (res) {
console.log(res, '分享完成');
}
};
}
},
methods: {
// 登录获取 cuk
async wxAutoLogin() {
const userStore = useUserStore();
await userStore.wxAutoLogin();
},
// 接收webview消息
getMessage(e) {
const {
data
} = e.detail;
console.log(data, "data")
if (data) {
this.share = data[data.length - 1]
}
},
// Webview加载完成
onWebviewLoad(e) {
console.log('Webview加载完成:', e);
if (this.pageTitle === '加载中...') {
this.pageTitle = '网页';
}
},
// Webview加载错误
onWebviewError(e) {
console.error('Webview加载错误:', e);
uni.showToast({
title: '页面加载失败',
icon: 'none'
});
},
// 返回上一页
goBack() {
uni.navigateBack({
delta: 1
});
}
}
}
</script>
<style lang="less" scoped>
.webview-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fff;
}
.webview-content {
flex: 1;
width: 100%;
}
</style>
......@@ -9,6 +9,7 @@ export const useHomeStore = defineStore("homeInfo", {
homeInfo: null,
isLogin: false,
babyExistence: false,
hasShownPopup: false, // 记录是否已经显示过弹窗(全局状态)
};
},
actions: {
......@@ -30,6 +31,20 @@ export const useHomeStore = defineStore("homeInfo", {
this.babyExistence = !babyExistence;
},
/**
* 标记弹窗已显示
*/
markPopupAsShown() {
this.hasShownPopup = true;
},
/**
* 重置弹窗状态(用于测试或特殊需求)
*/
resetPopupState() {
this.hasShownPopup = false;
},
/**
* 获取首页信息
*/
......@@ -38,8 +53,9 @@ export const useHomeStore = defineStore("homeInfo", {
console.log("loadHomeInfo", data);
if (data) {
this.setHomeInfo(data);
// 通过参数传入的方式使用 $sensors
// 通过参数传入的方式使用 $sensors
this.hasShownPopup = false
const userStore = useUserStore();
const mdData = {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -51,15 +51,26 @@
</view>
<!-- 积分账户 -->
<view class="integral-account">
<text class="integral-account-text"> 积分账户</text>
<text class="integral-account-text"> 账号</text>
<text class="integral-account-value">
{{ babyInfo.points || "0" }}</text>
{{ userStore.memberInfo?.mobile || '' }}</text
>
</view>
<BabySwitcher v-if="showBabySwitcher" :show="showBabySwitcher" :babyList="babyInfo.allBabyBaseInfo || []"
:addIcon="$baseUrl + 'my/baby_add_btn.png'" @close="showBabySwitcher = false" @select="onSelectBaby"
@add="onAddBaby" />
</view>
<view class="integralContainer">
<image
class="integralIcon"
src="https://course.feihe.com/momclub-picture/my/integralBg.png"
mode="aspectFit"
/>
<text class="integralText0">{{ babyInfo.points || "0" }}</text>
<text class="integralText1">积分</text>
</view>
</view>
<!-- user desc -->
<view v-if="cfgStatus.showDetail" class="user-desc">
......@@ -96,6 +107,28 @@
</view>
</view>
<view class="vip-active-area" v-if="pageCfgStore?.contentCfg?.activeInfo?.length > 0">
<text class="vip-title">精彩活动</text>
<swiper
class="vip-active-swiper"
:indicator-dots="pageCfgStore?.contentCfg?.activeInfo?.length > 1"
:autoplay="true"
:circular="true"
indicator-color="#dfddd9"
indicator-active-color="#b27c1e"
:indicator-top="32"
>
<swiper-item v-for="(item, index) in pageCfgStore?.contentCfg?.activeInfo" :key="index">
<image
class="vip-active-img"
:src="item?.img"
mode="aspectFit"
@click="handleVipActiveClick(index, item)"
/>
</swiper-item>
</swiper>
</view>
<!-- 协议 -->
<view class="protocol-container">
<image :src="$baseUrl + 'my/protocol.png'" mode="aspectFit" />
......@@ -358,7 +391,11 @@ onMounted(async () => {
pageName: "我的页面",
});
showLoading();
await userStore.loadMemberInfo();
await pageCfgStore.fetchCfg();
console.log("pageCfgStore.contentCfg====", pageCfgStore.contentCfg);
initData();
hideLoading();
......@@ -410,6 +447,38 @@ watch([() => userStore.userInfo, () => userStore.babyInfo], () => {
)?.id;
});
//会员活动点击事件
const handleVipActiveClick = (index, item) => {
// let buttonName = '';
// switch(index){
// case 0:
// buttonName = '第一张焦点图';
// break;
// case 1:
// buttonName = '第二张焦点图';
// break;
// case 2:
// buttonName = '第三张焦点图';
// break;
// }
// md.sensorLogTake({
// xcxClick: "积分服务页-二屏页面点击",
// pageName: "积分服务页-二屏",
// buttonName: buttonName,
// });
const url = item?.url;
const type = item?.type;
const extra = item?.extra;
jump({
type: type,
url: url,
extra:extra
})
}
// 定义页面配置
defineExpose({});
</script>
......@@ -555,8 +624,41 @@ defineExpose({});
color: #1d1e25;
}
}
}
.integralContainer {
position: absolute;
top: 0rpx;
right: 0rpx;
.integralIcon {
position: absolute;
width: 226rpx;
height: 166rpx;
right: 0rpx;
}
.integralText0 {
position: absolute;
font-size: 24rpx;
color: #1d1e25;
top: 45rpx;
right: 0rpx;
width: 167rpx;
text-align: center;
}
.integralText1 {
position: absolute;
font-size: 24rpx;
color: #1d1e25;
width: 167rpx;
text-align: center;
right: 0rpx;
top: 81rpx;
}
}
.edit-btn {
image {
width: 40rpx;
......@@ -595,7 +697,7 @@ defineExpose({});
border-radius: 32rpx;
padding: 32rpx 30rpx;
box-sizing: border-box;
margin-top: 161rpx;
margin-top: 180rpx;
.desc-top {
display: flex;
......@@ -694,6 +796,27 @@ defineExpose({});
}
}
.vip-active-area {
margin-left: 28rpx;
margin-top: 60rpx;
.vip-title {
margin-left: 20rpx;
}
.vip-active-swiper {
width: 692rpx;
height: 204rpx;
flex-shrink: 0;
margin-top: 30rpx;
margin-bottom: 100rpx;
.vip-active-img {
width: 100%;
height: 100%;
// border-radius: 16rpx;
}
}
}
.protocol-container {
width: 360rpx;
height: 25rpx;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment