uniapp开发之锚点定位,页面滑动菜单回显效果实现

一、需求背景

为了提升交互,在前端开发中经常会碰到,滚动/滑动页面时菜单回显的效果。有一天,产品找到我说uniapp中这个页面可以做到滑动后在上边按钮回显吗?我说能能,特此记录锚点定位的问题。

二、需求分析

实现锚点定位我们需要关注这几点:

1、当前各个分组dom的id
2、各个分组的dom的初始位置
3、获取当前滚动条的滚动距离
4、监听滚动事件,根据监听的的滚动距离和初始位置做比较,回显菜单

三、实现代码

1、我们先实现一个简单的例子。

先上效果图:
回显.gif

代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    <script type="text/javascript" src="https://cdn.bootcss.com/vConsole/3.3.0/vconsole.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16"></script>
    <style>
      .page-box {
        box-sizing: border-box;
        background-color: #fff;
        padding: 10px;
      }
      .container {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
      }
      .header {
        display: flex;
        gap: 10px;
        margin-bottom: 20px;
      }
      .button-style {
        height: 32px;
        line-height: 32px;
        width: 50px;
        text-align: center;
        font-size: 14px;
        border: 1px solid #189fff;
        border-radius: 3px;
        background-color: #fff;
        color: #189fff;
        cursor: pointer;
      }
      .main {
        height: 200px;
        overflow-y: scroll;
        width: 200px;
        border: 1px solid #a3a3a3;
        border-radius: 3px;
        display: flex;
        flex-direction: column;
        gap: 10px;
        position: relative;
      }
      .good-style {
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 0 10px;
        flex: none;
        height: 150px;
        width: 150px;
        text-align: center;
        font-size: 14px;
        border-radius: 3px;
        color: #ffffff;
      }
    </style>
  </head>
  <body>
    <div id="app" class="page-box">
      <div class="container">
        <div class="header">
          <div class="button-style" :style="showStatus(index)" v-for="(item, index) in buttonList" :key="item.code" @click="handleButtonClick(index)">
            {{item.name}}
          </div>
        </div>
        <div id="main" class="main">
          <div :id="'good_' + item.code" class="good-style" :style="{ 'background': item.background }" v-for="(item, index) in goodList" :key="item.code">
            {{item.name}}
          </div>
        </div>
      </div>
    </div>
    <script>
      let vue = new Vue({
        el: '#app',
        data() {
          return {
            activeButton: 0, // 当前选中index
            buttonList: [
              {
                name: '萝卜',
                code: 1
              },
              {
                name: '白菜',
                code: 2
              },
              {
                name: '鸡蛋',
                code: 3
              }
            ],
            goodList: [
              {
                name: '一个萝卜',
                code: 1,
                background: '#dc143c'
              },
              {
                name: '一个白菜',
                code: 2,
                background: '#3e82f7'
              },
              {
                name: '一个鸡蛋',
                code: 3,
                background: '#ffa500'
              }
            ],
            // 初始信息
            baseInfoList: []
          }
        },
        created() {
        },
        mounted() {
          // 获取dom的信息  
          this.getDomInfo()
          let dom = document.getElementById('main')
          // 监听滚动事件
          dom.addEventListener('scroll', this.handleScroll, false)
        },
        beforeDestroy() {
          let dom = document.getElementById('main')
          // 移除监听
          dom.removeEventListener('scroll', this.handleScroll)
        },
        methods: {
          // 激活的按钮添加颜色提示
          showStatus(index) {
            if(index === this.activeButton) {
              return {
                background: '#189fff',
                color: '#ffffff'
              }
            } else {
              return {}
            }
          },
          
          // 点击按钮回调
          handleButtonClick(data) {
            this.activeButton = data
            let dom = document.getElementById('main')
            dom.scrollTo({
              top: this.baseInfoList[data].top,
              behavior: 'smooth'
            })
          },
          
          // 获取dom原始信息
          getDomInfo() {
            this.baseInfoList = []
            this.goodList.forEach(x => {
              let dom = document.getElementById('good_' + x.code)
              this.baseInfoList.push({
                top: dom.offsetTop,
                height: dom.offsetHeight,
                id: 'good_' + x.code,
                code: x.code
              })
            })
          },
          
          // 处理滚动事件
          handleScroll(data) {
            console.dir(data.currentTarget.scrollTop)
            let scrollTop = data.currentTarget.scrollTop
            this.comparePosition(scrollTop)
          },
          
          // 比较滚动距离和dom初始位置
          comparePosition(scrollTop) {
            for (let i = 0; i < this.baseInfoList.length; i++) {
              if (i === 0) {
                if (scrollTop <= (this.baseInfoList[i].height * 1) / 2) {
                  this.activeButton = i
                }
              } else {
                if ((scrollTop <= this.baseInfoList[i].top + (this.baseInfoList[i].height * 1) / 2) &&
                 (scrollTop > this.baseInfoList[i].top - this.baseInfoList[i - 1].height / 2)) {
                  this.activeButton = i
                }
              }
            }
          }
        }
      })
    </script>
  </body>
</html>
2、uniapp页面开发时需要注意和h5的不同之处:

1) uniapp有自带的监听页面滚动的方法 onPageScroll
2) uniapp创建的app不支持浏览器端专属的window、document、navigator等对象,需要
let query = uni.createSelectorQuery().in(this) 通过该方法查询dom信息
3)exec() 方法仅需要最后执行一次,不要多次执行。

示例代码:(仅供比对参考,具体冗余业务代码没删除,复制后无法单独运行)

<template>
	<view class="page-box">
		<navigator-bar :showBack="true">
			<template #content>
				<tab-list :tabList="tabList"></tab-list>
			</template>
		</navigator-bar>
		<view class="container" :style="{ paddingTop: (statusbarHeight + navHeight) + 'px' }">
			<view>
				<view class="plan-detail">
					<view class="title">
						<view class="left">
							<text class="text">{{ !validateNull(userModel.cons_name) ? userModel.cons_name : "" }}</text>
							<u-icon name="edit-pen" class="edit" size="24" color="#ff5c26" @click="showEditModel"></u-icon>
						</view>
						<u-button v-if="recheck" size="mini" class="last-btn" shape="circle" @click="lastCheck">上次安检记录</u-button>
					</view>
					<u-row gutter="10">
						<u-col span="12">
							<view class="item-col">
								<image mode="widthFix" class="list-icon" :src="userImg" width="30rpx" height="30rpx"></image>
								<text class="text">用户号:{{ !validateNull(userModel.cons_no) ? userModel.cons_no : "" }}</text>
							</view>
						</u-col>
					</u-row>
					<u-row gutter="10">
						<u-col span="6">
							<view class="item-col">
								<image mode="widthFix" class="list-icon" :src="telImg" width="30rpx" height="30rpx"></image>
								<text class="text">电话:{{ !validateNull(userModel.cons_tel) ? userModel.cons_tel : "" }}</text>
							</view>
						</u-col>
						<u-col span="6">
							<view class="item-col">
								<image mode="widthFix" class="list-icon" :src="statusImg" width="30rpx" height="30rpx"></image>
								<text class="text">用户状态:</text>
								<text class="text"
									:class="{ created: userModel.cons_status === 1, closed: userModel.cons_status !== 1 }">{{
										userModel.cons_status
										=== 1 ? "正常" : "异常" }}</text>
							</view>
						</u-col>
					</u-row>
					<u-row gutter="10">
						<u-col span="12">
							<view class="item-col">
								<image mode="widthFix" class="list-icon" :src="addressImg" width="30rpx" height="30rpx"></image>
								<text class="text">详细地址:{{ !validateNull(userModel.cons_addr) ? userModel.cons_addr : "" }}</text>
							</view>
						</u-col>
					</u-row>
					<u-row gutter="10">
						<u-col span="12">
							<view class="item-col">
								<image mode="widthFix" class="list-icon" :src="countImg" width="30rpx" height="30rpx"></image>
								<text class="text">备注:{{ !validateNull(userModel.description) ? userModel.description : "" }}</text>
							</view>
						</u-col>
					</u-row>
				</view>
				<!-- 点击按钮滚动到指定区域 -->
				<scroll-view class="model-list" v-if="modelCode.groupInfoList && modelCode.groupInfoList.length" scroll-x="true"
					:scroll-with-animation="true" :scroll-left="scrollLeft"
					:style="{ position: 'sticky', top: (statusbarHeight + navHeight) + 'px' }" @scroll="scroll">
					<view class="model-item" v-for="(item, index) in modelCode.groupInfoList" :key="item.id"
						:class="{ active: index === currentIndex }" @click="checkTab(item, index)">
						<view class="img">
							<u--image :src="baseUrl + item.group_image" :lazy-load="true" mode="widthFix" width="55rpx"
								height="55rpx"></u--image>
						</view>
						<text class="name">{{ item.group_name }}</text>
					</view>
				</scroll-view>
				<template v-if="modelCode.groupInfoList && modelCode.groupInfoList.length">

					<view class="model-group" v-for="(item, index) in modelCode.groupInfoList" :key="item.id"
						:id="'into' + index">
						<view class="title" :class="{ active: index === currentIndex }">
							<view class="left">
								<u-badge :type="index === currentIndex ? 'error' : 'primary'" max="99" :value="index + 1"
									shape="horn"></u-badge>
								<text class="text">{{ item.group_name }}</text>
							</view>
							<view class="right">
								<u-switch size="20" v-if="getInstallItemVal(item.itemList) !== '2'"
									@change="e => changeInstallVal(e, item.itemList)" :value="getInstallItemVal(item.itemList)"
									activeValue="1" inactiveValue="0" activeColor="#61C589"></u-switch>
							</view>
						</view>
						<view v-if="getInstallItemVal(item.itemList) !== '0'">
							<view class="cell-box" v-for="(param, idx) in filterItemList(item.itemList)" :key="param.id"
								@click="cellTap(index, idx, param.item_type)">
								<view class="show-box">
									<view class="u-slot-title">
										<text v-if="param.required_flag" class="isMust">*</text>
										<text class="u-cell-text">{{ param.item_name }}</text>
									</view>
									<view class="right-model">
										<model-params :paramValue.sync="param.paramValue" :showModel.sync="showModel"
											:paramCheckValue.sync="param.paramCheckValue" :type="param.item_type"
											:defaultVal="param.default_value" :plh="param.placeholder" :options="param.item_option"
											@updateModelVal="val => updateModelVal(val, param)"></model-params>
									</view>
								</view>
								<view class="hidden-box" v-if="showHiddenBox(param)">
									<view class="hidden-cell">
										<view class="u-slot-title">
											<text class="u-cell-text">整改措施:</text>
										</view>
										<view class="right-model">
											{{ param.rectification_method }}
										</view>
									</view>
									<view class="hidden-cell">
										<view class="u-slot-title">
											<text class="u-cell-text">处理方式:</text>
										</view>
										<view class="right-model">
											<picker @change="e => confirmPicker(e, param)" :value="param.hiddenObj.rectMethod"
												range-key="label" :range="reportList">
												<view class="model-select">
													<text v-if="reportList[param.hiddenObj.rectMethod]">{{
														reportList[param.hiddenObj.rectMethod].label }}</text>
													<text v-else class="plh">请选择</text>
													<u-icon name="arrow-right" color="#c6c6c6" size="18"></u-icon>
												</view>
											</picker>
										</view>
									</view>
									<view class="hidden-cell">
										<view class="u-slot-title">
											<text class="u-cell-text">备注:</text>
										</view>
										<view class="right-model">
											<u--textarea class="model-area" v-model="param.hiddenObj.desc" placeholder="请输入备注"
												height="50"></u--textarea>
										</view>
									</view>
									<view class="hidden-cell">
										<view class="u-slot-title">
											<text class="u-cell-text">隐患拍照:</text>
										</view>
										<view class="right-model">
											<view class="upload-box">
												<u-upload :fileList="param.hiddenObj.hiddenImg" :capture="['camera']"
													@afterRead="e => afterRead(e, item, param, 'hidden')" :previewImage="true"
													@delete="e => deleteHiddenPic(e, param, 'hidden')" name="hidden" multiple
													:maxCount="6"></u-upload>
											</view>
										</view>
									</view>
									<view v-if="param.hiddenObj.rectMethod === 0" class="hidden-cell">
										<view class="u-slot-title">
											<text class="u-cell-text"><text style="color: red; padding-right: 3px;">*</text>整改后拍照:</text>
										</view>
										<view class="right-model">
											<view class="upload-box">
												<u-upload :fileList="param.hiddenObj.hiddenAfterImg" :capture="['camera']"
													@afterRead="e => afterRead(e, item, param, 'hiddenAfter')" :previewImage="true"
													@delete="e => deleteHiddenPic(e, param, 'hiddenAfter')" name="hidden" multiple
													:maxCount="6"></u-upload>
											</view>
										</view>
									</view>
								</view>
							</view>
							<view class="cell-box" @click="d(item)">
								<view class="show-box">
									<view class="u-slot-title">
										<text class="u-cell-text"><span style="color: red;padding-right: 3px;">*</span>拍照:</text>
									</view>
									<view class="right-model">
										<view class="upload-box">
											<u-upload name="photo" multiple :previewImage="true" :fileList="item.imgList" :maxCount="6"
												@afterRead="e => afterRead(e, item)" :capture="['camera']"
												@delete="e => deletePic(e, index)"></u-upload>
										</view>
									</view>
								</view>
							</view>
						</view>
					</view>
				</template>
				<dy-nodata v-else showType="default"></dy-nodata>
			</view>
		</view>
		<view class="submit">
			<u-button size="mini" class="btn" shape="circle" @click="signAndSubmit" :loading="loading">提 交</u-button>
		</view>
		<u-modal :show="showEdit" @confirm="confirmEdit" ref="uModal" :asyncClose="true" title="修改客户信息"
			:showCancelButton="true" @cancel="showEdit = false" :borderBottom="false" confirmColor="#F4801A">
			<u--form labelPosition="left" :model="editForm" :rules="rules" ref="editForm" class="edit-form">
				<u-form-item label="" prop="consName" borderBottom ref="consName">
					<u--input placeholder="请输入用户名称" border="surround" prefixIcon="account"
						prefixIconStyle="font-size: 32rpx;color: #ef1e0b" v-model="editForm.consName"></u--input>
				</u-form-item>
				<!-- <u-form-item label="" prop="consAddr" borderBottom ref="consAddr">
					<u--input placeholder="请填写地址" border="surround" prefixIcon="map"
						prefixIconStyle="font-size: 32rpx;color: #ef1e0b" v-model="editForm.consAddr"></u--input>
				</u-form-item> -->
				<u-form-item label="" prop="consTel" borderBottom ref="consTel">
					<u--input placeholder="请输入手机号码" border="surround" prefixIcon="phone"
						prefixIconStyle="font-size: 32rpx;color: #ef1e0b" v-model="editForm.consTel"></u--input>
				</u-form-item>
				<u-form-item label="" prop="consAddr" borderBottom ref="consAddr">
					<u--input placeholder="请输入详细地址" border="surround" prefixIcon="map"
						prefixIconStyle="font-size: 32rpx;color: #ef1e0b" v-model="editForm.consAddr"></u--input>
				</u-form-item>
				<u-form-item label="" prop="description" borderBottom ref="description">
					<u--input placeholder="请填写备注信息" border="surround" prefixIcon="list-dot"
						prefixIconStyle="font-size: 32rpx;color: #ef1e0b" v-model="editForm.description"></u--input>
				</u-form-item>
			</u--form>
		</u-modal>
		<sign-in v-if="showMap" :signType="signType" @close="closeMapSign" @sign="submitCheck"></sign-in>
	</view>
</template>

<script>
	import {
		hideTabBar,
		validateNull,
		parseTime
	} from '@/common/utils.js'
	import {
		BASE_FILE_URL
	} from "@/common/api.js"
	import statusImg from "@/static/security/status.png"
	import userImg from "@/static/security/user.png"
	import telImg from "@/static/security/tel.png"
	import addressImg from "@/static/security/address.png"
	import countImg from "@/static/security/count.png"
	export default {
		data() {
			return {
				statusImg,
				userImg,
				telImg,
				addressImg,
				countImg,
				// 顶部状态栏高度
				statusbarHeight: uni.getSystemInfoSync().statusBarHeight,
				// 顶部导航栏高度
				navHeight: getApp().globalData.ifIOS ? 44 : 48,
				// 顶部tab切换列表
				tabList: [{
					name: "入户",
					title: ""
				}],
				baseUrl: BASE_FILE_URL,
				modelCode: {}, // 根据模板ID查询的模板代码列表
				userModel: {}, // 根据用户ID查询的用户信息
				consId: 0, // 客户id
				templateId: 0, // 模板ID
				planId: 0, // 安检计划id
				intoView: "", // 锚点跳转
				scrollLeft: 0,
				oldScroll: 0,
				currentIndex: 0, // 选中的模型
				// indexs: [0, 0], // 当前点击安检项的索引
				// 现场整改、上报维修、下达整改通知
				reportList: [{
					name: 0,
					label: "现场整改"
				}, {
					name: 1,
					label: "上报维修"
				}, {
					name: 2,
					label: "下达整改通知"
				}],
				showModel: "", // 是否显示数据项组件
				showEdit: false, // 是否显示编辑
				editForm: {
					consName: "",
					consAddr: "",
					consTel: "",
					description: ""
				},
				rules: {
					'consName': {
						type: 'string',
						required: true,
						message: '请输入用户名称',
						trigger: ['blur']
					},
					'consAddr': {
						type: 'string',
						required: true,
						message: '请填写地址',
						trigger: ['blur']
					},
					'consTel': [{
						required: true,
						message: '请输入手机号码',
						trigger: ['blur']
					}, {
						// 自定义验证函数,见上说明
						validator: (rule, value, callback) => {
							// 上面有说,返回true表示校验通过,返回false表示不通过
							// uni.$u.test.mobile()就是返回true或者false的
							return uni.$u.test.mobile(value);
						},
						message: '手机号码不正确',
						// 触发器可以同时用blur和change
						trigger: ['blur']
					}]
				},
				recheck: "", // 是否是复检
				// 是否显示签到弹窗
				showMap: false,
				// 签到类型
				signType: '1', // 安检
				
				// 为了适应现状临时用到的变量 start
				sign_address: '',
				longitude: '',
				latitude: '',
				// 为了适应现状临时用到的变量 end
				// 提交按钮loading
				loading: false,
        // dom的初始位置
        itemTopBaseList: [],
        // dom的id列表
        idList: [],
        // 是否处理监听
        scrollFlag: true,
        // 存储第一个未填必填项的位置
        firstNoticeItemInfo: null
			}
		},
		created() {
			// 打开页面时隐藏掉tabbar 使用自定义tabbar
			hideTabBar()
		},
		computed: {
			// 过滤出不带是否安装的项
			filterItemList() {
				return function(list) {
					return list.filter(x => x.item_code !== 'installed')
				}
			}
		},
		onLoad(option) {
			if (option.templateId) {
				this.templateId = option.templateId
			}
			if (option.consId) {
				this.consId = option.consId
			}
			if (option.planId) {
				this.planId = option.planId
			}
			if (option.recheck) {
				this.recheck = option.recheck
			}
			// 页面每次显示时走一下数据
			this.resetSearch()
			this.getUserModel()
		},
		onShow() {},
    onPageScroll(data) {
      if (this.scrollFlag) {
        let info = this.compareScroll(data.scrollTop)
        if (info !== null) {
          let index = info.index
          this.scrollLeft = this.oldScroll + ((index - Math.floor(this.modelCode.groupInfoList.length / 2)) * 74)
          this.currentIndex = index
          this.intoView = info.id
        }
      }
    },
		methods: {
      compareScroll(scrollTop) {
        if (this.itemTopBaseList.length) {
          for (let i = 0; i < this.itemTopBaseList.length; i++) {
            if (i === 0) {
              if (scrollTop < this.itemTopBaseList[0].top - 244 + Number(((this.itemTopBaseList[0].height * 3) / 4).toFixed(0))) {
                return this.itemTopBaseList[0]
              } 
            } else {
              if (scrollTop < this.itemTopBaseList[i].top - 244 + Number(((this.itemTopBaseList[i].height * 3) / 4).toFixed(0)) &&
              scrollTop >= this.itemTopBaseList[i - 1].top - 244 + Number(((this.itemTopBaseList[i - 1].height * 3) / 4).toFixed(0))) {
                return this.itemTopBaseList[i]
              }
            }
          }
        }
        return null
      },
			d(item) {
			},
			// 获取列表中是否安装 项 的值
			getInstallItemVal(arr) {
				let itemArr = arr.filter(x => x.item_code === "installed")
				return itemArr.length ? itemArr[0].paramValue : "2"
			},
			// 切换是否安装
			changeInstallVal(e, arr) {
				let itemArr = arr.filter(x => x.item_code === "installed")
				if (itemArr.length) {
					this.$set(itemArr[0], 'paramValue', itemArr[0].paramValue === "1" ? "0" : "1")
				}
			},
			scroll: function(e) {
				this.oldScroll = e.detail.scrollLeft
			},
			validateNull(val) {
				return validateNull(val)
			},
			// 重置查询
			resetSearch() {
				this.getModelCode()
			},
			// 根据用户ID查询用户详情
			getUserModel() {
				let param = {
					cons_id: this.consId
				}
				this.$http("GET", "mobile/securityPlan/qryConsDetails", param)
					.then(res => {
						if (res.success) {
							this.userModel = res.result
						} else {
							this.$showToast(res.message)
						}
					})
					.catch(err => {
						this.$showToast(err.message)
					})
			},
      // 获取巡检组和定位父级的距离信息
      getNodeBaseTop() {
        let query = uni.createSelectorQuery().in(this)
        this.$nextTick(() => {
          this.idList = []
          this.itemTopBaseList = [] // 距离定位父级的距离
          if (this.modelCode.groupInfoList && this.modelCode.groupInfoList.length > 0) {
            for(let i = 0; i < this.modelCode.groupInfoList.length; i++) {
              this.idList.push({id: 'into' + i, key: this.modelCode.groupInfoList[i].id, index: i})
              let nodeDom = query.select(`#into${i}`)
              nodeDom.fields({
                id: true,
                rect: true,
                size: true,
                scrollOffset: true
              }, (data) => {
                this.itemTopBaseList.push({
                  id: data.id,
                  top: data.top,
                  height: data.height,
                  index: i
                })
              })
            }
          }
          query.exec()
        })
      },

			// 根据模板id查询模板代码项
			getModelCode() {
				let param = {
					id: this.templateId
				}
				this.$http("GET", "mobile/securityTemplate/load", param)
					.then(res => {
						if (res.success) {
							this.modelCode = res.result
							this.modelCode.groupInfoList.forEach(item => {
								// 添加照片集合字段
								this.$set(item, "imgList", [])
								// 是否展示子项
								// this.$set(item, "showFlag", true)
								item.itemList.forEach(opt => {
									// 添加默认赋值属性 paramValue
									if (opt.item_type === "checkbox") {
										this.$set(opt, "paramCheckValue", opt.default_value ? opt.default_value.split(",") : [])
									} else {
										this.$set(opt, "paramValue", opt.default_value ? opt.default_value : "")
									}
									// 添加隐患信息字段
									this.$set(opt, "hiddenObj", {
										desc: "",
										rectMethod: 0, // 整改方式 0、现场整改 1、上报维修 2、下达隐患通知
										hiddenImg: [],
										hiddenAfterImg: []
									})
								})
							})
              this.getNodeBaseTop() // 获取巡检组的基础dom信息
						} else {
							this.$showToast(res.message)
						}
					})
					.catch(err => {
						this.$showToast(err.message)
					})
			},
			// 触顶
			upperHandle() {
				console.log("触顶了")
			},
			// 触底 分页增加 重新查询
			lowerHandle() {
				console.log("触底了")
			},
			// 点击模型列表
			checkTab(item, index) {
        this.scrollFlag = false
				this.scrollLeft = this.oldScroll + ((index - Math.floor(this.modelCode.groupInfoList.length / 2)) * 74)
				this.currentIndex = index
				this.intoView = 'into' + index
				this.point()
			},
			// 点击cell列表
			cellTap(index, idx, type) {
				this.showModel = type
				// this.indexs = [index, idx]
			},
			// 更新数据
			updateModelVal(val, param) {
				this.$set(param, "paramValue", val)
			},
			// 确认选择 处理方式
			confirmPicker(e, param) {
				this.$set(param.hiddenObj, "rectMethod", e.target.value)
			},
			// 删除图片
			deletePic(event, index) {
				this.modelCode.groupInfoList[index].imgList.splice(event.index, 1)
				// this[`fileList${event.name}`].splice(event.index, 1)
			},
			// 删除图片
			deleteHiddenPic(event, param, type) {
				if (type === 'hiddenAfter') {
					param.hiddenObj.hiddenAfterImg.splice(event.index, 1)
				} else if (type === 'hidden') {
					param.hiddenObj.hiddenImg.splice(event.index, 1)
				}
			},
			// 新增安检图片 | 隐患照片上传
			async afterRead(event, item, param, type) {
				// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
				let lists = [].concat(event.file)
				let arr
				if (validateNull(param)) {
					arr = item.imgList
				} else {
					if (type === 'hidden') {
						arr = param.hiddenObj.hiddenImg
					} else if (type === 'hiddenAfter') {
						arr = param.hiddenObj.hiddenAfterImg
					}
				}
				let fileListLen = arr.length
				lists.map((item) => {
					arr.push({
						url: item.url
					})
				})
				for (let i = 0; i < lists.length; i++) {
					const result = await this.uploadFilePromise(lists[i].url)
					let item = arr[fileListLen]
					arr.splice(fileListLen, 1, Object.assign(item, {
						status: 'success',
						message: '',
						photo_url: result,
						url: BASE_FILE_URL + result
					}))
					fileListLen++
				}
			},
			uploadFilePromise(url) {
				return this.$uploadFile(url, "mobile/fileUpload")
			},
			// 是否显示隐患项
			showHiddenBox(item) {
				let flag = item.hidden_danger_flag === 1
				let great = item.hidden_danger_judge === "greater" && item.paramValue > item.hidden_danger_value
				let equal = item.hidden_danger_judge === "equal" && item.paramValue === item.hidden_danger_value
				let less = item.hidden_danger_judge === "less" && item.paramValue < item.hidden_danger_value
				let notNull = item.hidden_danger_judge === "notNull" && !validateNull(item.paramValue)
				if (item.item_type === "radio") {
					return flag && equal
				} else if (item.item_type === "checkbox") {
					let arr = item.hidden_danger_value ? item.hidden_danger_value.split(",") : []
					if (item.paramCheckValue && item.paramCheckValue.length) {
						return item.paramCheckValue.some(c => arr.indexOf(c) > -1)
					} else {
						return false
					}
				}
				return flag && (great || equal || less || notNull)
			},
			
			signAndSubmit() {
				// this.showMap = true // 临时禁用
				
				// 临时启用 start
				this.getLocation()
				// 临时启用 end
			},
			
			// 提交
			submitCheck(signInfo) {
        this.firstNoticeItemInfo = null
				// 处理提交参数
				let param = {
					planId: this.planId,
					accountId: this.consId,
					repetitionFlag: 0,
					state: "normal",
					elecSignature: "",
					description: "",
					securityCheckItemValueList: [],
					securityCheckRecordPhotoList: [],
					hiddenDangerList: []
				}
				let state // 状态 normal | danger
				let checkItems = [] // 安检项集合
				let checkPhotos = [] // 安检照片集合参数
				let hiddenDangers = [] // 隐患信息
				let isValidate = true
				for (let item of this.modelCode.groupInfoList) {
					item.imgList.forEach(m => {
						checkPhotos.push({
							group_id: item.id,
							photo_url: m.photo_url
						})
					})
					let installItemVal = this.getInstallItemVal(item.itemList)
					for(let opt of item.itemList){
						if (opt.required_flag && installItemVal !== '0') {
							if ((opt.item_type === "checkbox" && !opt.paramCheckValue.length) || (opt.item_type !== "checkbox" &&
									!opt.paramValue)) {
                let temp = this.idList.filter(idFlag => {
                  return idFlag.key === item.id
                })
                if (temp && temp.length > 0) {
                  this.firstNoticeItemInfo = temp[0]
                  this.checkTab({}, this.firstNoticeItemInfo.index)
                }
								isValidate = false
							}
						}
						let isHidden = this.showHiddenBox(opt)
						if (opt.item_type === "checkbox" && opt.paramCheckValue.length) {
							checkItems.push({
								item_id: opt.id,
								item_value: opt.paramCheckValue.join(",")
							})
						} else if (opt.paramValue) {
							checkItems.push({
								item_id: opt.id,
								item_value: opt.paramValue
							})
						}
						if (isHidden) {
							state = true
							let hiddenPhotos = []
							let hiddenAfterPhotos = [] // 现场整改完成后的拍照
							let dangerObj = {
								currentItem: opt,
								groupId: item.id,
								groupName: item.group_name,
								itemId: opt.id,
								infoSource: "item",
								handleMethod: this.reportList[opt.hiddenObj.rectMethod].label,
								// handleMethodCode: opt.hiddenObj.rectMethod,
								description: opt.hiddenObj.desc
							}
							opt.hiddenObj.hiddenImg.forEach(h => {
								hiddenPhotos.push({
									photo_url: h.photo_url,
									hidden_danger_photo_type: 'UnhandledPhoto'
								})
							})
							if (opt.hiddenObj.hiddenAfterImg && opt.hiddenObj.hiddenAfterImg.length < 1 && opt.hiddenObj.rectMethod === 0) {
                let temp = this.idList.filter(idFlag => {
                  return idFlag.key === item.id
                })
                if (temp && temp.length > 0) {
                  this.firstNoticeItemInfo = temp[0]
                  this.checkTab({}, this.firstNoticeItemInfo.index)
                }
								this.$showToast("现场整改的隐患项需要上传整改后照片!")
								return
							}
							opt.hiddenObj.hiddenAfterImg.forEach(h => {
								hiddenAfterPhotos.push({
									photo_url: h.photo_url,
									hidden_danger_photo_type: 'HandledPhoto'
								})
							})
							if (opt.hiddenObj.rectMethod === 1) {
								dangerObj.workOrderRepair = {
									work_content: opt.hidden_danger_judge === "notNull" ? opt.paramValue : opt.rectification_method
								}
							}
							dangerObj.hiddenDangerInfoPhotoList = [...hiddenPhotos, ...hiddenAfterPhotos]
							// dangerObj.hiddenAfterDangerInfoPhotoList = hiddenAfterPhotos
							hiddenDangers.push(dangerObj)
						}
					}
				}
				
				if (!isValidate) {
					this.$showToast("请完善必填字段!")
					return
				}
				param.state = state ? "danger" : "normal"
				param.securityCheckItemValueList = checkItems
				param.securityCheckRecordPhotoList = checkPhotos
				param.hiddenDangerList = hiddenDangers
				if (signInfo) {
					param.clockInRecord = signInfo
				} else {
					delete param.clockInRecord
				}
				let homeInParams = encodeURIComponent(JSON.stringify(param))
				uni.setStorageSync("homeInParams", homeInParams)
				this.$navigatorPage('/pages/sc-confirm/sc-confirm')
			},
			lastCheck() {
				this.$navigatorPage("/pages/security-view/security-view?&templateId=" + this.templateId + "&accountId=" + this
					.userModel.id + "&recheck=" + this.recheck)
			},
			showEditModel() {
				this.showEdit = true
				this.editForm.consName = this.userModel.cons_name
				this.editForm.consAddr = this.userModel.cons_addr
				this.editForm.consTel = this.userModel.cons_tel
			},
			// 确认编辑 保存
			confirmEdit() {
				this.$refs.editForm.validate().then(res => {
					let editData = {
						id: this.userModel.id,
						cons_name: this.editForm.consName,
						cons_addr: this.editForm.consAddr,
						cons_tel: this.editForm.consTel,
						description: this.editForm.description
					}
					this.$http('POST', 'mobile/consAccount/update', editData).then(res => {
						this.$showToast(res.message)
						this.showEdit = false
						this.getUserModel()
					})
				}).catch(errors => {
				})
			},
			point(index) { //锚点滑动 
				uni.createSelectorQuery().select(`#${this.intoView}`).boundingClientRect(data => { //目标位置节点 类或者 id
					uni.createSelectorQuery().select(".model-group").boundingClientRect((res) => { //最外层盒子节点类或者 id
						uni.pageScrollTo({
							duration: 300, //过渡时间
							scrollTop: data.top - res.top + 160, //到达距离顶部的top值
              complete: () => {
                setTimeout(() => {
                  this.scrollFlag = true
                }, 400)
              }
						})
					}).exec()
				}).exec();
			},
			
			/**
			 * 关闭签到弹窗的回调
			 */
			closeMapSign() {
				this.showMap = false
			},
			
			// 为了适应现状进行的妥协,写下屈辱的方法, 临时启用,后续会禁用 start
			getLocation() {
				this.sign_address = ''
				this.longitude = ''
				this.latitude = ''
				let temp = ''
				this.loading = true
				uni.getLocation({
					type: 'gcj02',
					geocode: true,
					isHighAccuracy: true,
					highAccuracyExpireTime: 5000,
					success: (data) => {
						if (data.address) {
							this.sign_address = data.address.province + data.address.city + data.address.district + data.address.street + data.address.streetNum + data.address.poiName
						}
						this.longitude = data.longitude
						this.latitude = data.latitude
						// 签到时间 signTime
						let nowDate = new Date().getTime()
						this.signTime = parseTime(nowDate, 'zh')
						if(this.signType && this.longitude && this.latitude) {
							temp = {
								clock_in_type: this.signType,
								clock_in_addr: this.sign_address,
								longitude: this.longitude,
								latitude: this.latitude
							}
						}
					},
					fail: () => {
						console.log('获取位置失败');
					},
					complete: () => {
						this.loading = false
						console.log('完成位置获取工作');
						this.submitCheck(temp)
					}
				})
			},
			// 为了适应现状进行的妥协,写下屈辱的方法, 临时启用,后续会禁用 end
		}
	}
</script>

<style lang="scss" scoped>
	page {
		background-color: #f5f5f5;
	}

	.placeholder {
		height: 510rpx;
		width: 100%;
	}

	.container {
		width: 100%;
		padding: 0 0 16rpx;
		background-color: #f5f5f5;
		margin-bottom: 110rpx;

		.search {
			width: 100%;
			padding: 20rpx 30rpx;
			background: #fff;
		}

		.scroll-box {
			height: 100%;
			scroll-behavior: smooth;
		}

	}

	.plan-detail {
		width: 100%;
		padding: 30rpx 30rpx 10rpx;
		background: #f5f5f5;
		display: flex;
		flex-direction: column;
		justify-content: center;

		.title {
			display: flex;
			justify-content: space-between;
			align-items: center;
			margin-bottom: 20rpx;
			font-size: 28rpx;
			color: #333;

			.left {
				display: flex;
				align-items: center;
				flex-direction: row;

				.text {
					margin-right: 10rpx;
				}
			}

			.u-button {
				width: 160rpx;
				margin: 0;
				color: $normal_red;
				border: none;
			}

			.last-btn {
				width: 260rpx;
				height: 56rpx;
				background: #fff;
				border: 1px solid $normal_red;
				color: $normal_red;
				font-size: 28rpx;
			}
		}

		.item-col {
			width: 100%;
			display: flex;
			align-items: center;
			flex-direction: row;
			justify-content: flex-start;
			margin-bottom: 20rpx;

			.text {
				font-size: 28rpx;
				color: #333;

				&.closed {
					color: $normal_red;
				}

				&.created {
					color: #61C589;
				}
			}

			.list-icon {
				width: 30rpx;
				height: 30rpx;
				margin-right: 10rpx;
			}
		}
	}

	.model-list {
		white-space: nowrap;
		scroll-behavior: smooth;
		height: 200rpx;
		padding: 25rpx 30rpx;
		background-color: #fff;
		box-sizing: border-box;
		// position: fixed!important;
		// top: 402rpx;
		z-index: 10 !important;

		.model-item {
			display: inline-block;
			width: 100rpx;
			margin-right: 48rpx;
			text-align: center;

			&:last-child {
				margin: 0;
			}

			.img {
				overflow: hidden;
				width: 100rpx;
				height: 100rpx;
				margin-bottom: 10rpx;
				display: flex;
				justify-content: center;
				align-items: center;
				background-color: #F4F4F7;
				border-radius: 14rpx;
			}

			.name {
				font-size: 28rpx;
				color: #232832;
			}

			&.active {
				.img {
					background-color: $normal_red;
				}

				.name {
					color: $normal_red;
				}
			}
		}
	}

	.model-group {
		padding: 0 0 22rpx;
		margin-top: 16rpx;
		background-color: #fff;
		overflow-y: overlay;

		.title {
			display: flex;
			justify-content: space-between;
			align-items: center;
			padding: 20rpx 30rpx;
			color: #333;

			.left {
				display: flex;
				justify-content: space-between;
				align-items: center;
			}

			.text {
				margin-left: 10rpx;
			}

			&.active {
				color: $normal_red;
			}
		}

		// .unable {
		// 	pointer-events: none;
		// 	opacity: 0.4;
		// }

		.isMust {
			color: $normal_red;
		}
	}

	.cell-box {
		position: relative;
		width: 100%;
		padding: 0 30rpx 1px;

		&::after {
			content: "";
			position: absolute;
			left: 30rpx;
			right: 30rpx;
			bottom: 1px;
			height: 1px;
			background-color: #c6c6c6;
		}

		&:last-child {
			&::after {
				background-color: #fff;
			}
		}

		.show-box {
			display: flex;
			justify-content: space-between;
			align-items: center;
			width: 100%;
			min-height: 40rpx;
			padding: 25rpx 0;
			background: #fff;
			gap: 0rpx;
		}

		.hidden-box {
			padding: 10rpx 20rpx;
			background-color: #f4f4f4;

			.hidden-cell {
				display: flex;
				justify-content: space-between;
				align-items: center;
				width: 100%;
				min-height: 40rpx;
				padding: 25rpx 0;
			}
		}
	}

	.u-slot-title,
	.right-model {
		white-space: nowrap;
		color: #333;
		font-size: 28rpx;

		.model-area {
			width: 300rpx;
		}
	}

	.upload-box {
		width: 360rpx;

		/deep/ .u-upload__wrap {
			justify-content: flex-end;
		}

		/deep/ .u-upload__wrap__preview__image {
			width: 100rpx !important;
			height: 100rpx !important;
		}
	}

	.model-select {
		display: flex;
		flex-direction: row;
		align-items: center;
	}

	/deep/ .u-upload__button {
		width: 100rpx !important;
		height: 100rpx !important;
		background-color: #fff;
		border: 1px solid #c6c6c6;
		border-radius: 12rpx;
	}

	.submit {
		width: 100%;
		height: 120rpx;
		text-align: center;
		padding: 30rpx 0 36rpx;
		background-color: #fff;
		position: fixed;
		bottom: 0;

		.btn {
			width: 160rpx;
			height: 56rpx;
			background: $normal_red;
			border-color: $normal_red;
			color: #fff;
			font-size: 28rpx;
		}
	}

	.edit-form {
		width: 100%;
	}

	/deep/ .u-modal__title {
		height: 80rpx;
		line-height: 80rpx;
		padding-top: 0;
		background: linear-gradient(180deg, #F98665 0%, $normal_red 100%);
		color: #fff;
	}
</style>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值