场景
在平板端没有很好的适配组件,所以自己写了一部分,放置可惜,发出来可以给大家借鉴
实现
1.下拉选择框

<view class="page-device-switch">
<view class="device-picker flex flex-justify-between" @tap="showDeviceDropdown = !showDeviceDropdown">
{{ nickname || '请选择设备' }}
<i class="iconfont icon-xiajiantou" :class="{'icon-rotate': showDeviceDropdown}" style="margin-left: 8rpx;font-size: 15rpx;color:#000;"></i>
</view>
<view v-if="showDeviceDropdown" class="device-dropdown">
<view v-for="(item, idx) in allDeviceList" :key="item.id" class="device-dropdown-item" @tap="selectDevice(idx)">
{{ item.nickname }}
</view>
</view>
</view>
...
data() {
return {
allDeviceList:[], //设备列表
showDeviceDropdown: false,
}
},
selectDevice(idx) {
this.selectedDeviceIndex = idx;
this.nickname = this.allDeviceList[idx].nickname;
this.showDeviceDropdown = false;
},
...
.page-device-switch{
height: 30rpx;
width: 140rpx;
position: absolute;
left: 10.25rpx;
top: 54.65rpx;
}
.device-picker {
display: flex;
align-items: center;
height: 20rpx;
padding: 0 5.86rpx;
background: rgba($color: #fff, $alpha: 0.8);
border-radius: 5.86rpx;
color: #000;
font-size: 13rpx;
min-width: 200rpx;
position: relative;
z-index: 2;
.iconfont {
transition: transform 0.3s ease;
}
.icon-rotate {
transform: rotate(180deg);
}
}
.device-picker .iconfont {
color: #000;
}
.device-dropdown {
position: absolute;
margin-top: 8rpx;
background: rgba($color: #fff, $alpha: 0.8);
border-radius: 5.86rpx;
box-shadow: 0 2rpx 8rpx rgba(30,144,255,0.08);
border: 1rpx solid #ccc;
min-width: 200rpx;
z-index: 10;
max-height: 300rpx;
overflow-y: auto;
}
.device-dropdown-item {
padding: 8rpx 14rpx;
font-size: 13rpx;
color: #000;
cursor: pointer;
transition: background 0.2s;
background: transparent;
}
.device-dropdown-item:hover {
background: #e6f2ff;
}
2.侧滑抽屉

<!-- 自定义左侧侧滑抽屉 -->
<transition name="drawer-slide">
<view v-if="showDeviceDrawer" class="custom-drawer-mask" @tap.self="showDeviceDrawer = false">
<view class="custom-drawer-panel" @tap.stop>
<view class="drawer-title">选择设备</view>
<view class="drawer-device-list">
<view
v-for="(item, idx) in allDeviceList"
:key="item.id"
class="drawer-device-item"
:class="{ active: idx === selectedDeviceIndex }"
@tap="handleSelectDevice(idx)"
>
<span class="device-dot" :class="{ active: idx === selectedDeviceIndex }"></span>
<text class="device-name">{{ item.nickname }}</text>
<i v-if="idx === selectedDeviceIndex" class="iconfont icon-duigou"></i>
</view>
</view>
</view>
<view class="custom-drawer-right-mask" @tap="showDeviceDrawer = false"></view>
</view>
</transition>
...
data() {
return {
allDeviceList:[],
showDeviceDrawer: false,
}
},
handleSelectDevice(idx) {
this.selectedDeviceIndex = idx;
this.nickname = this.allDeviceList[idx].nickname;
this.showDeviceDrawer = false;
},
...
.custom-drawer-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.25);
z-index: 9999;
display: flex;
align-items: stretch;
}
.custom-drawer-panel {
width: 320rpx;
height: 100vh;
background: #fff;
box-shadow: 2rpx 0 16rpx rgba(0,0,0,0.08);
padding: 24rpx 0 0 0;
display: flex;
flex-direction: column;
animation: drawerIn 0.25s;
}
.drawer-title {
font-size: 22rpx;
font-weight: bold;
color: #222;
padding: 0 24rpx 18rpx 24rpx;
border-bottom: 1rpx solid #eee;
}
.drawer-device-list {
flex: 1;
overflow-y: auto;
padding: 10rpx 0;
max-height: calc(100vh - 60rpx);
}
.drawer-device-item {
padding: 12rpx 18rpx;
font-size: 18rpx;
color: #333;
display: flex;
align-items: center;
cursor: pointer;
border-radius: 6rpx;
margin-bottom: 2rpx;
transition: background 0.2s, color 0.2s;
min-height: 36rpx;
}
.drawer-device-item.active {
background: #e6f2ff;
color: #2979ff;
font-weight: bold;
}
.device-dot {
display: inline-block;
width: 10rpx;
height: 10rpx;
border-radius: 50%;
margin-right: 12rpx;
background: #333;
flex-shrink: 0;
transition: background 0.2s;
}
.drawer-device-item.active .device-dot {
background: #2979ff;
}
.device-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drawer-device-item .icon-duigou {
color: #2979ff;
margin-left: 8rpx;
font-size: 18rpx;
}
.drawer-device-item:not(.active):hover {
background: #f5f7fa;
}
.custom-drawer-right-mask {
flex: 1;
height: 100vh;
}
.drawer-slide-enter-active, .drawer-slide-leave-active {
transition: all 0.25s;
}
.drawer-slide-enter, .drawer-slide-leave-to {
opacity: 0;
}
@keyframes drawerIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
3.触底加载

<view class="content flex flex-wrap flex-content-start">
<scroll-view
scroll-y="true"
@scrolltolower="load"
style="width: 100%; height: 100%;"
>
<view class="box" v-for="(item, index) in tableData" :key="index">
<view class="title">{{ item.date }}</view>
<view class="cards">
<view
class="card"
v-for="v in item.children"
:key="v.id"
@click="previewMedia(v)"
>
<view class="file">
<!-- 图片类型 -->
//...
<!-- 视频类型 -->
//...
<!-- 文件夹类型 -->
//...
</view>
<view class="name end-h">{{ v.fileName }}</view>
</view>
</view>
</view>
</scroll-view>
</view>
...
data() {
return {
tableData: [],
isLoading: false,
current: 1,
size:10,
total:0,
}
},
mounted() {
this.getDatas()
this.load()
},
load() {
if (this.isLoading) return;
this.isLoading = true;
setTimeout(() => {
this.current += 1;
this.getDatas();
}, 1000);
},
getDatas(){
if(!this.subTypeString){
this.tableData = [];
return;
}
requestName(params).then(res=>{
if(res.data && res.data.records.length){
this.isLoading = false;
this.total = res.data.total;
let arr = res.data.records.map(item=>{
item.date = item.createTime.substring(0,10);
item.isCheck = false;
if(item.fileUrl.includes('_T_')||item.fileUrl.includes('_T.')){
item.type = 'ir'
}else {
item.type = 'other'
}
return item;
});
let data = this.tableData;
for(let i=0;i<arr.length;i++){
if(!data.filter(v=> v.date == arr[i].date).length){
data.push({
date:arr[i].date,
children:[arr[i]],
})
}else{
for(let j=0;j<data.length;j++){
if(data[j].date==arr[i].date){
data[j].children.push(arr[i])
}
}
}
}
this.tableData = data.sort((a, b) => {
let dateA = new Date(a.date);
let dateB = new Date(b.date);
return dateB - dateA;
});;
if (this.tableData.length >= this.total) {
this.finished = true;
}
}
}).catch(()=>{
this.isLoading = true;
})
},
4.树状数据表
内嵌图片和视频两种媒体文件;图片文件点击预览,打开可缩放,长按可下载;视频文件自动播放初始桢,可全屏可下载




5.弹窗+功能


6. 悬浮按钮

<!-- 扩展图标按钮 -->
<view class="more flex flex-align-center flex-justify-center" @longpress="showActionMenu($event, item, index)">
<i class="iconfont icon-gengduo2" style="color:#8C8C8C;font-size: 10.25rpx;"></i>
</view>
<!-- 自定义悬浮菜单 -->
<view v-if="showMenu" class="action-menu" :style="menuStyle" @tap.stop>
<view class="action-menu-item" @tap="handleMenuAction('view', currentItem, currentIndex)">
<text>查看航线</text>
</view>
<view class="action-menu-item" @tap="handleMenuAction('execute', currentItem, currentIndex)">
<text>执行航线</text>
</view>
<view class="action-menu-item" @tap="handleMenuAction('delete', currentItem, currentIndex)">
<text>删除航线</text>
</view>
</view>
<!-- 遮罩层 -->
<view v-if="showMenu" class="menu-mask" @tap="hideActionMenu"></view>
...
data(){
return {
showMenu: false,
currentItem: null,
currentIndex: -1,
menuStyle: {},
}
}
showActionMenu(event, item, index) {
this.currentItem = item
this.currentIndex = index
this.showMenu = true
// 获取点击位置
const touch = event.touches[0] || event.changedTouches[0]
const x = touch.clientX
const y = touch.clientY
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
const windowWidth = systemInfo.windowWidth
const windowHeight = systemInfo.windowHeight
// 菜单尺寸(预估)
const menuWidth = 120 // rpx转px大约60px
const menuHeight = 120 // 三个选项的高度
// 计算菜单位置,避免超出屏幕边界
let left = x - menuWidth / 2
let top = y - menuHeight - 10 // 在点击位置上方显示
// 边界检测
if (left < 10) left = 10
if (left + menuWidth > windowWidth - 10) left = windowWidth - menuWidth - 10
if (top < 10) top = y + 10 // 如果上方空间不够,显示在下方
this.menuStyle = {
position: 'fixed',
left: left + 'px',
top: top + 'px',
zIndex: 9999
}
},
hideActionMenu() {
this.showMenu = false
this.currentItem = null
this.currentIndex = -1
},
handleMenuAction(action, item, index) {
this.hideActionMenu()
switch(action) {
case 'view':
this.clickCard(item)
break
case 'execute':
this.executeWayline(item)
break
case 'delete':
this.deleteWayline(item, index)
break
}
},
...
// 自定义悬浮菜单样式
.action-menu {
background: #333333;
border-radius: 6rpx;
padding: 6rpx 0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
min-width: 120rpx;
.action-menu-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 12rpx;
color: #FFFFFF;
font-size: 10rpx;
white-space: nowrap;
&:active {
background-color: #58595B;
}
&:not(:last-child) {
border-bottom: 1rpx solid #4A4A4A;
}
text {
font-size: 10rpx;
color: #FFFFFF;
}
}
}
.menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 9998;
}
- 模块配置器

<!-- 模块配置按钮 -->
<view class="module-config-btn" :class="{ active: showModuleConfig }" @tap="showModuleConfig = true">
<i class="iconfont icon-shezhi" style="font-size: 16rpx;color: #333333;"></i>
</view>
<!-- 模块展示卡片 部分代码-->
<view class="entrance-box flex flex-wrap flex-justify-between">
<view
v-for="module in visibleModules"
:key="module.id"
:class="['entrance-card', module.bgClass]"
@tap="goOtherPage(module.id)"
>
<text class="entrance-title">{{ module.title }}</text>
<view class="entrance-icon flex flex-justify-center flex-align-center">
<i :class="['iconfont', module.icon]" style="font-size: 26.37rpx;color: #333333"></i>
</view>
</view>
</view>
<!-- 模块配置面板 -->
<transition name="modal-fade">
<view v-if="showModuleConfig" class="module-config-mask" @tap.self="showModuleConfig = false">
<view class="module-config-panel" @tap.stop>
<view class="config-header">
<text class="config-title">模块配置</text>
<view class="config-close" @tap="showModuleConfig = false">
<i class="iconfont icon-guanbi" style="font-size: 16rpx;"></i>
</view>
</view>
<view class="config-content">
<view class="config-tip">拖拽排序,点击开关,最多选择4个模块</view>
<view class="module-list">
<view
v-for="module in allModules"
:key="module.id"
class="module-item"
:class="{ disabled: !module.visible && visibleModules.length >= 4 }"
@tap="toggleModule(module.id)"
>
<view class="module-drag-handle">
<i class="iconfont icon-drag" style="font-size: 14rpx;color: #999;"></i>
</view>
<view class="module-icon">
<i :class="['iconfont', module.icon]" style="font-size: 20rpx;color: #333;"></i>
</view>
<text class="module-name">{{ module.title }}</text>
<view class="module-switch" :class="{ active: module.visible }">
<view class="switch-dot"></view>
</view>
</view>
</view>
</view>
<view class="config-footer">
<view class="config-btn cancel-btn" @tap="resetModuleConfig">重置</view>
<view class="config-btn confirm-btn" @tap="saveModuleConfig">确定</view>
</view>
</view>
</view>
</transition>
...
data() {
return {
// 所有可用模块
allModules: [
{
id: 1,
title: '设备',
icon: 'icon-wurenji',
bgClass: 'entrancebg1',
visible: true
},
{
id: 2,
title: '航线库',
icon: 'icon-hangxian3',
bgClass: 'entrancebg2',
visible: true
},
{
id: 3,
title: '飞行任务',
icon: 'icon-renwujincheng',
bgClass: 'entrancebg3',
visible: true
},
{
id: 4,
title: '媒体库',
icon: 'icon-meitiku2',
bgClass: 'entrancebg4',
visible: true
},
{
id: 5,
title: '营区资质',
icon: 'icon-yingqu_ic',
bgClass: 'entrancebg5',
visible: false
}
]
}
},
computed: {
// 计算可见的模块(最多4个)
visibleModules() {
return this.allModules.filter(module => module.visible).slice(0, 4);
}
},
mounted() {
this.loadModuleConfig()
},
goOtherPage(val){
if(val==1){
uni.navigateTo({
url: '../shebeigl/shebeigl'
});
}
if(val==2){
uni.navigateTo({
url: '../hangxiank/hangxiank'
});
}
if(val==3){
uni.navigateTo({
url: '../feixingrw/feixingrw'
});
}
if(val==4){
uni.navigateTo({
url: '../meitik/meitik'
});
}
if(val==5){
uni.navigateTo({
url: '../yingquzz/yingquzz'
});
}
},
// 模块配置相关方法
toggleModule(moduleId) {
const module = this.allModules.find(m => m.id === moduleId);
if (!module) return;
// 如果当前模块已显示,直接关闭
if (module.visible) {
module.visible = false;
return;
}
// 如果当前模块未显示,但已达到4个限制,需要先关闭一个
if (this.visibleModules.length >= 4) {
uni.showToast({
title: '最多只能选择4个模块',
icon: 'none',
duration: 2000
});
return;
}
module.visible = true;
},
saveModuleConfig() {
// 保存配置到本地存储
const config = this.allModules.map(module => ({
id: module.id,
visible: module.visible
}));
uni.setStorageSync('moduleConfig', config);
this.showModuleConfig = false;
uni.showToast({
title: '配置已保存',
icon: 'success',
duration: 1500
});
},
resetModuleConfig() {
// 重置为默认配置
this.allModules.forEach((module, index) => {
module.visible = index < 4; // 前4个默认显示
});
uni.showToast({
title: '已重置为默认配置',
icon: 'success',
duration: 1500
});
},
loadModuleConfig() {
// 从本地存储加载配置
const savedConfig = uni.getStorageSync('moduleConfig');
if (savedConfig && Array.isArray(savedConfig)) {
savedConfig.forEach(config => {
const module = this.allModules.find(m => m.id === config.id);
if (module) {
module.visible = config.visible;
}
});
}
},
...
.module-config-btn {
height: 29.3rpx;
width: 29.3rpx;
position: absolute;
left: 13.92rpx;
top: 24.17rpx;
border-radius: 50%;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
&.active {
background: #FF4444;
.iconfont {
color: #FFFFFF !important;
}
}
}
.entrance-box{
width: 402.83rpx;
height: 100%;
.entrance-card{
width: 197.02rpx;
height: 191.16rpx;
border-radius: 5.86rpx;
padding: 11.72rpx;
position: relative;
.entrance-title{
font-family: 'PingFang SC';
font-weight: 500;
font-size: 17.58rpx;
color: #FFFFFF;
}
.entrance-icon{
position: absolute;
bottom: 11.72rpx;
right: 11.72rpx;
height: 46.88rpx;
width: 46.88rpx;
border-radius: 50%;
background-color: #EDEFF2;
}
}
.entrance-card:nth-child(3){
margin-top: 8.79rpx;
}
.entrance-card:nth-child(4){
margin-top: 8.79rpx;
}
.entrancebg1{
background: url('../../static/images/s-shebei.png') center center / 100% 100%;
}
.entrancebg2{
background: url('../../static/images/s-hangxian.png') center center / 100% 100%;
}
.entrancebg3{
background: url('../../static/images/s-feixing.png') center center / 100% 100%;
}
.entrancebg4{
background: url('../../static/images/s-meitik.png') center center / 100% 100%;
}
.entrancebg5{
background: url('../../static/images/s-yingquzz.jpg') center center / 100% 100%;
}
}
/* 模块配置面板样式 */
.module-config-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.module-config-panel {
width: 500rpx;
max-height: 70vh;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
animation: modalIn 0.3s ease-out;
}
.config-header {
height: 60rpx;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
border-bottom: 1rpx solid #eee;
.config-title {
font-size: 16rpx;
font-weight: bold;
color: #333;
}
.config-close {
width: 28rpx;
height: 28rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f0f0f0;
color: #666;
transition: all 0.2s;
&:active {
background: #e0e0e0;
transform: scale(0.95);
}
}
}
.config-content {
padding: 16rpx 20rpx;
max-height: 50vh;
overflow-y: auto;
.config-tip {
font-size: 11rpx;
color: #666;
text-align: center;
margin-bottom: 12rpx;
padding: 8rpx 12rpx;
background: #f8f9fa;
border-radius: 6rpx;
}
.module-list {
.module-item {
height: 48rpx;
display: flex;
align-items: center;
padding: 0 12rpx;
margin-bottom: 8rpx;
background: #f8f9fa;
border-radius: 8rpx;
transition: all 0.2s;
position: relative;
&:active {
transform: scale(0.98);
}
&.disabled {
opacity: 0.5;
background: #f0f0f0;
}
.module-drag-handle {
width: 20rpx;
height: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
opacity: 0.6;
}
.module-icon {
width: 28rpx;
height: 28rpx;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 6rpx;
margin-right: 8rpx;
box-shadow: 0 1rpx 3rpx rgba(0,0,0,0.1);
}
.module-name {
flex: 1;
font-size: 13rpx;
color: #333;
font-weight: 500;
}
.module-switch {
width: 36rpx;
height: 20rpx;
background: #ddd;
border-radius: 10rpx;
position: relative;
transition: all 0.3s ease;
&.active {
background: #007AFF;
.switch-dot {
transform: translateX(16rpx);
}
}
.switch-dot {
width: 16rpx;
height: 16rpx;
background: #fff;
border-radius: 50%;
position: absolute;
top: 2rpx;
left: 2rpx;
transition: all 0.3s ease;
box-shadow: 0 1rpx 3rpx rgba(0,0,0,0.2);
}
}
}
}
}
.config-footer {
height: 60rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
border-top: 1rpx solid #eee;
background: #f8f9fa;
.config-btn {
height: 36rpx;
padding: 0 20rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 13rpx;
font-weight: 500;
transition: all 0.2s;
&.cancel-btn {
background: #f0f0f0;
color: #666;
&:active {
background: #e0e0e0;
transform: scale(0.95);
}
}
&.confirm-btn {
background: #007AFF;
color: #fff;
&:active {
background: #0056CC;
transform: scale(0.95);
}
}
}
}
/* 动画效果 */
.modal-fade-enter-active, .modal-fade-leave-active {
transition: all 0.3s ease;
}
.modal-fade-enter, .modal-fade-leave-to {
opacity: 0;
transform: scale(0.9);
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
3259

被折叠的 条评论
为什么被折叠?



