自定义悬浮式Tabbar实现Navigation的自动隐藏与显示

66 篇文章 ¥59.90 ¥99.00
本文介绍如何使用SwiftUI创建一个悬浮式Tabbar,并在导航时自动隐藏和显示。通过示例代码,展示了如何在ContentView中设置Tabbar的隐藏状态,以及如何在Tabbar视图中响应点击事件,同时利用修饰符调整视图布局。

在本文中,我们将讨论如何使用SwiftUI自定义一个悬浮式Tabbar,并实现在导航过程中自动隐藏和显示Tabbar的功能。我们将提供相应的源代码示例,以便您更好地理解实现的过程。

首先,让我们创建一个新的SwiftUI项目。在Xcode中,选择"创建新项目",选择"App"模板,并确保选择使用SwiftUI进行界面设计。

在项目创建完成后,我们将开始自定义悬浮式Tabbar的实现。打开项目中的ContentView.swift文件,并进行以下更改:

import SwiftUI

struct ContentView: View {
   
   
    @State private var isTabbarHidden = <
<template> <view class="page-container"> <scroll-view style="flex:1" scroll-y="true" :scroll-bottom="scrollBottom" @scroll="onScroll"> <view class="work-container"> <view class="card-list"> <view class="card-item" v-for="(item, index) in approvalList" :key="index"> <view class="card-content"> <view class="item-type">{{ item.type }}</view> <view class="item-status" :data-status="item.status">{{ getStatusText(item.status) }}</view> <view class="item-detail" @tap="handleDetailClick(item)">详情</view> <view class="checkbox-wrapper"> <view class="custom-checkbox" :class="{ 'checked': selectedItems[index] }" @tap="toggleSelect(index)"> </view> </view> </view> </view> <!-- 列表底部占位,给签核区域留空间 --> <view class="list-bottom-placeholder" :style="{ height: hasSelected ? '180rpx' : '0' }"></view> </view> </view> </scroll-view> <!-- 独立悬浮在tabbar上方的签核区域 --> <view class="floating-action-bar" :class="{ 'show': hasSelected }"> <view class="floating-content"> <!-- 关闭按钮 --> <view class="close-btn" @tap="clearSelection"> <text class="close-icon">×</text> </view> <!-- 左侧驳回按钮 --> <view class="action-btn reject-btn" @tap="handleReject"> <text class="btn-icon">✗</text> <text class="btn-text">驳回</text> </view> <!-- 中间分隔和信息 --> <view class="action-center"> <view class="selected-info"> <view class="info-dot"></view> <text class="info-text">已选 {{ selectedCount }} 项</text> </view> </view> <!-- 右侧通过按钮 --> <view class="action-btn approve-btn" @tap="handleApprove"> <text class="btn-icon">✓</text> <text class="btn-text">通过</text> </view> </view> <!-- 悬浮区域底部的装饰 --> <view class="floating-bottom"></view> </view> </view> </template> <script lang="uts"> import { getApprovalList } from '@/api/approval/review' interface ApprovalItem { id : number type : string status : string data : string } export default { data() { return { approvalList: [] as ApprovalItem[], selectedItems: [] as boolean[], scrollBottom: '0' } }, computed: { // 计算是否有选中的项目 hasSelected() : boolean { return this.selectedItems.some(item => item) }, // 计算选中数量 selectedCount() : number { return this.selectedItems.filter(item => item).length } }, onShow() { this.fetchApprovalList() }, methods: { async fetchApprovalList() { try { const data = await getApprovalList() if (data && data.data) { this.approvalList = data.data as ApprovalItem[] // 初始化选择状态数组 this.selectedItems = new Array(this.approvalList.length).fill(false) } } catch (error) { console.error('获取审批列表失败:', error) } }, getStatusText(status : string) : string { const statusMap : Map<string, string> = new Map([ ["0", "待审批"], ["1", "已通过"], ["2", "已拒绝"], ["11", "处理中"] ]) return statusMap.get(status.trim()) || status }, toggleSelect(index : number) { this.selectedItems[index] = !this.selectedItems[index] // 使用 $set 确保响应式更新 this.$set(this.selectedItems, index, this.selectedItems[index]) // 如果有选中项目,自动滚动到底部 if (this.hasSelected) { setTimeout(() => { this.scrollBottom = '99999' }, 100) } }, handleDetailClick(item : ApprovalItem) { const params = { id: item.id, type: item.type, status: item.status, data: item.data } const paramsStr = encodeURIComponent(JSON.stringify(params)) uni.navigateTo({ url: `/pages/approval/type/info?params=${paramsStr}`, success: () => { console.log('跳转到审批详情成功', item.id) }, fail: (err) => { console.error('跳转到审批详情失败', err) uni.showToast({ title: '跳转失败', icon: 'none' }) } }); }, // 清空所有选择 clearSelection() { this.selectedItems = new Array(this.selectedItems.length).fill(false) }, onScroll(e : any) { // 可以添加滚动逻辑 }, // 处理通过 handleApprove() { const selectedIds = this.approvalList .filter((_, index) => this.selectedItems[index]) .map(item => item.id) if (selectedIds.length === 0) return uni.showModal({ title: '确认通过', content: `确定要通过选中的 ${selectedIds.length} 个审批项吗?`, success: (res) => { if (res.confirm) { // 这里调用通过审批的API console.log('通过审批项:', selectedIds) uni.showToast({ title: '审批通过', icon: 'success' }) // 清空选中状态 this.clearSelection() } } }) }, // 处理驳回 handleReject() { const selectedIds = this.approvalList .filter((_, index) => this.selectedItems[index]) .map(item => item.id) if (selectedIds.length === 0) return uni.showModal({ title: '确认驳回', content: `确定要驳回选中的 ${selectedIds.length} 个审批项吗?`, success: (res) => { if (res.confirm) { // 这里调用驳回审批的API console.log('驳回审批项:', selectedIds) uni.showToast({ title: '已驳回', icon: 'none' }) // 清空选中状态 this.clearSelection() } } }) } } } </script> <style lang="scss"> .page-container { display: flex; flex-direction: column; height: 89vh; position: relative; } .work-container { padding: 24rpx; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); flex: 1; .card-list { .card-item { background: linear-gradient(135deg, #ffffff 0%, #fcfdff 100%); border-radius: 28rpx; padding: 36rpx 32rpx; margin-bottom: 24rpx; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08), 0 3rpx 12rpx rgba(0, 0, 0, 0.04), inset 0 1rpx 0 rgba(255, 255, 255, 0.8); border: 1rpx solid rgba(229, 231, 235, 0.8); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; /* 左侧装饰条 */ &::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 6rpx; height: 60%; background: linear-gradient(to bottom, #1890ff, #36cfc9); border-radius: 0 8rpx 8rpx 0; } &:active { transform: translateY(-2rpx) scale(0.995); box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.12), 0 4rpx 16rpx rgba(0, 0, 0, 0.06), inset 0 1rpx 0 rgba(255, 255, 255, 0.8); } .card-content { flex: 1; display: flex; flex-direction: row; align-items: center; gap: 32rpx; padding-left: 16rpx; .item-type { font-size: 34rpx; font-weight: 700; color: #1e293b; letter-spacing: 0.2rpx; min-width: 140rpx; position: relative; padding-left: 4rpx; /* 类型文字装饰 */ &::after { content: ''; position: absolute; left: 0; bottom: -4rpx; width: 40rpx; height: 3rpx; background: linear-gradient(to right, #1890ff, transparent); border-radius: 2rpx; } } .item-status { font-size: 26rpx; padding: 10rpx 24rpx; border-radius: 32rpx; font-weight: 600; letter-spacing: 0.2rpx; min-width: 96rpx; text-align: center; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08), inset 0 1rpx 0 rgba(255, 255, 255, 0.4); border: 1rpx solid transparent; transition: all 0.2s ease; &[data-status="0"] { background: linear-gradient(135deg, #ffd666, #ffc53d); color: #d46b08; border-color: rgba(255, 197, 61, 0.3); text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5); } &[data-status="1"] { background: linear-gradient(135deg, #b7eb8f, #95de64); color: #389e0d; border-color: rgba(149, 222, 100, 0.3); text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5); } &[data-status="2"] { background: linear-gradient(135deg, #ffccc7, #ffa39e); color: #cf1322; border-color: rgba(255, 163, 158, 0.3); text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5); } &[data-status="11"] { background: linear-gradient(135deg, #bae7ff, #91d5ff); color: #096dd9; border-color: rgba(145, 213, 255, 0.3); text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5); } } .item-detail { font-size: 28rpx; color: #64748b; font-weight: 500; padding: 10rpx 24rpx; background: linear-gradient(135deg, #f8fafc, #f1f5f9); border-radius: 24rpx; border: 1rpx solid rgba(226, 232, 240, 0.8); transition: all 0.2s ease; min-width: 80rpx; text-align: center; &:active { background: linear-gradient(135deg, #e2e8f0, #cbd5e1); color: #475569; } } } .checkbox-wrapper { margin-left: 32rpx; position: relative; .custom-checkbox { width: 44rpx; height: 44rpx; border-radius: 12rpx; border: 2rpx solid #e2e8f0; background: linear-gradient(135deg, #ffffff, #f8fafc); position: relative; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 3rpx 10rpx rgba(0, 0, 0, 0.05), inset 0 1rpx 0 rgba(255, 255, 255, 0.8); /* 复选框内阴影 */ &::before { content: ''; position: absolute; top: 2rpx; left: 2rpx; right: 2rpx; bottom: 2rpx; border-radius: 8rpx; background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.4)); } &.checked { border-color: #1890ff; background: linear-gradient(135deg, #1890ff, #36cfc9); transform: rotate(90deg) scale(1.05); box-shadow: 0 4rpx 15rpx rgba(24, 144, 255, 0.3), 0 2rpx 6rpx rgba(24, 144, 255, 0.2); &::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20rpx; height: 20rpx; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; opacity: 0; animation: checkAppear 0.2s ease 0.1s forwards; } } } } /* 卡片悬停效果 */ &:hover { transform: translateY(-3rpx); box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.12), 0 4rpx 16rpx rgba(0, 0, 0, 0.08), inset 0 1rpx 0 rgba(255, 255, 255, 0.8); border-color: rgba(148, 163, 184, 0.6); .item-detail { transform: translateY(-1rpx); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); } } } /* 列表底部占位符 */ .list-bottom-placeholder { transition: height 0.3s ease; } } } /* 悬浮在tabbar上方的签核区域 */ .floating-action-bar { position: fixed; left: 24rpx; right: 24rpx; bottom: calc(100rpx + 24rpx); /* 在tabbar上方24rpx */ z-index: 1000; /* 高于tabbar */ transform: translateY(200rpx); opacity: 0; transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); pointer-events: none; &.show { transform: translateY(0); opacity: 1; pointer-events: auto; } .floating-content { background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border-radius: 24rpx; padding: 70rpx 24rpx 30rpx 24rpx; box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.15), 0 4rpx 20rpx rgba(0, 0, 0, 0.08), 0 2rpx 10rpx rgba(0, 0, 0, 0.04); border: 1rpx solid rgba(229, 231, 235, 0.9); display: flex; flex-direction: row; align-items: center; position: relative; overflow: hidden; /* 顶部的装饰条 */ &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4rpx; background: linear-gradient(90deg, #ff6b6b, #1890ff, #1dd1a1); border-radius: 24rpx 24rpx 0 0; } /* 关闭按钮 */ .close-btn { position: absolute; top: 12rpx; right: 12rpx; width: 36rpx; height: 36rpx; border-radius: 50%; background: rgba(0, 0, 0, 0.05); display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; cursor: pointer; z-index: 10; &:active { background: rgba(0, 0, 0, 0.1); transform: scale(0.9); } .close-icon { font-size: 28rpx; font-weight: 300; color: #64748b; line-height: 1; } } /* 按钮样式 */ .action-btn { flex: 1; display: flex; flex-direction: row; align-items: center; justify-content: center; padding: 24rpx 20rpx; border-radius: 20rpx; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; min-width: 0; position: relative; z-index: 1; &:active { transform: translateY(4rpx) scale(0.98); } .btn-icon { font-size: 44rpx; font-weight: 300; margin-bottom: 8rpx; line-height: 1; transition: transform 0.2s ease; } .btn-text { font-size: 32rpx; font-weight: 700; letter-spacing: 1rpx; } /* 驳回按钮样式 */ &.reject-btn { background: linear-gradient(135deg, #ff6b6b, #ff4757); color: white; box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.3); &:active { background: linear-gradient(135deg, #ff4757, #ff3838); box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4); .btn-icon { transform: scale(0.9); } } } /* 通过按钮样式 */ &.approve-btn { background: linear-gradient(135deg, #1dd1a1, #10ac84); color: white; box-shadow: 0 4rpx 16rpx rgba(29, 209, 161, 0.3); &:active { background: linear-gradient(135deg, #10ac84, #0a8f6d); box-shadow: 0 2rpx 8rpx rgba(29, 209, 161, 0.4); .btn-icon { transform: scale(0.9); } } } } /* 中间信息区域 */ .action-center { flex: 0 0 auto; padding: 0 24rpx; min-width: 180rpx; .selected-info { display: flex; flex-direction: column; align-items: center; justify-content: center; .info-dot { width: 16rpx; height: 16rpx; background: linear-gradient(135deg, #1890ff, #36cfc9); border-radius: 50%; margin-bottom: 10rpx; position: relative; &::after { content: ''; position: absolute; top: -4rpx; left: -4rpx; right: -4rpx; bottom: -4rpx; border-radius: 50%; border: 2rpx solid rgba(24, 144, 255, 0.3); animation: ripple 2s infinite; } } .info-text { font-size: 26rpx; font-weight: 600; color: #1e293b; text-align: center; line-height: 1.2; } } } } /* 底部装饰 */ .floating-bottom { height: 8rpx; background: linear-gradient(135deg, #ffffff, #f8fafc); border-radius: 0 0 24rpx 24rpx; margin: 0 24rpx; opacity: 0.8; filter: blur(4rpx); transform: translateY(-2rpx); } } /* 动画 */ @keyframes checkAppear { from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes ripple { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.8); opacity: 0; } } /* 暗色模式适配 */ @media (prefers-color-scheme: dark) { .work-container { background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); .card-item { background: linear-gradient(135deg, #1e293b 0%, #334155 100%); border-color: rgba(71, 85, 105, 0.6); box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3), 0 3rpx 12rpx rgba(0, 0, 0, 0.2), inset 0 1rpx 0 rgba(255, 255, 255, 0.05); &::before { background: linear-gradient(to bottom, #36cfc9, #0891b2); } .card-content { .item-type { color: #f1f5f9; &::after { background: linear-gradient(to right, #36cfc9, transparent); } } .item-status { box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3), inset 0 1rpx 0 rgba(255, 255, 255, 0.1); text-shadow: 0 1rpx 1rpx rgba(0, 0, 0, 0.3); &[data-status="0"] { background: linear-gradient(135deg, #ffa940, #fa8c16); color: #fff2e8; border-color: rgba(255, 169, 64, 0.4); } &[data-status="1"] { background: linear-gradient(135deg, #73d13d, #52c41a); color: #f6ffed; border-color: rgba(115, 209, 61, 0.4); } &[data-status="2"] { background: linear-gradient(135deg, #ff7875, #ff4d4f); color: #fff2f0; border-color: rgba(255, 120, 117, 0.4); } &[data-status="11"] { background: linear-gradient(135deg, #69c0ff, #1890ff); color: #e6f7ff; border-color: rgba(105, 192, 255, 0.4); } } .item-detail { color: #cbd5e1; background: linear-gradient(135deg, #334155, #475569); border-color: rgba(71, 85, 105, 0.8); &:active { background: linear-gradient(135deg, #475569, #64748b); color: #e2e8f0; } } } .checkbox-wrapper { .custom-checkbox { border-color: #475569; background: linear-gradient(135deg, #334155, #1e293b); box-shadow: 0 3rpx 10rpx rgba(0, 0, 0, 0.3), inset 0 1rpx 0 rgba(255, 255, 255, 0.05); &::before { background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.1)); } &.checked { border-color: #36cfc9; background: linear-gradient(135deg, #36cfc9, #0891b2); box-shadow: 0 4rpx 15rpx rgba(54, 207, 201, 0.3), 0 2rpx 6rpx rgba(54, 207, 201, 0.2); } } } &:hover { border-color: rgba(100, 116, 139, 0.8); box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.4), 0 4rpx 16rpx rgba(0, 0, 0, 0.3), inset 0 1rpx 0 rgba(255, 255, 255, 0.05); } } } .floating-action-bar { .floating-content { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-color: rgba(71, 85, 105, 0.6); box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.4), 0 4rpx 20rpx rgba(0, 0, 0, 0.3), 0 2rpx 10rpx rgba(0, 0, 0, 0.2); &::before { background: linear-gradient(90deg, #ff6b6b, #36cfc9, #1dd1a1); } .close-btn { background: rgba(255, 255, 255, 0.1); &:active { background: rgba(255, 255, 255, 0.2); } .close-icon { color: #cbd5e1; } } .action-btn { &.reject-btn { background: linear-gradient(135deg, #ff6b6b, #ff3838); box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.4); &:active { background: linear-gradient(135deg, #ff3838, #d63031); box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.5); } } &.approve-btn { background: linear-gradient(135deg, #1dd1a1, #0a8f6d); box-shadow: 0 4rpx 16rpx rgba(29, 209, 161, 0.4); &:active { background: linear-gradient(135deg, #0a8f6d, #076552); box-shadow: 0 2rpx 8rpx rgba(29, 209, 161, 0.5); } } } .action-center { .selected-info { .info-dot { background: linear-gradient(135deg, #36cfc9, #0891b2); &::after { border-color: rgba(54, 207, 201, 0.4); } } .info-text { color: #f1f5f9; } } } } .floating-bottom { background: linear-gradient(135deg, #1e293b, #0f172a); } } } </style> 这段代码是uniapp x项目中的uvue页面内容,其实就是移动端app的一个页面,当前有一个小问题就是approvalList中数据不只有几条,而当前页面最多只能显示9条,以至于超过9条的部分页面无法展示,分析一下页面结构,能不能以滚动条的形式看到其他数据,注意整个页面的头部是存在一个head的,底部有一个可以切换页面的tabbar组件
最新发布
12-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值