congming_huose-apk/common/components/ControlTab.vue

742 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="control-tab">
<!-- 顶部状态卡片 -->
<view class="status-card">
<view class="" v-if="kjobj.status == 1"> <image style="width: 60rpx;height: 60rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uzZLS6VfQDFA1jTNaZVq" mode="aspectFill"></image> </view>
<view class="" v-if="kjobj.status == 2"> <image style="width: 56rpx;height: 60rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uuGVupItvaHcUcDfB8Dw" mode="aspectFill"></image> </view>
<view class="" v-if="kjobj.status == 3"> <image style="width: 60rpx;height: 60rpx;" src="https://api.ccttiot.com/smartmeter/img/static/u69cK2hDbhRQ9BQAEysP" mode="aspectFill"></image> </view>
<view class="" v-if="kjobj.status == 4"> <image style="width: 60rpx;height: 60rpx;" src="https://api.ccttiot.com/smartmeter/img/static/urRtZWuAS07btljjIrty" mode="aspectFill"></image> </view>
<text class="status-title">{{ titlename }}</text>
<!-- <text class="status-subtitle">{{ $i18n.t('alarmCleared') }}</text> -->
</view>
<!-- 控制栅格卡片 -->
<view class="grid-card">
<view class="grid-item" @click="onOpenAlarm">
<text class="grid-label">{{ $i18n.t('armAlarm') }}</text>
<image style="width: 102rpx;height: 102rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uyM0CBeinHHNAs5lp3NF" mode="aspectFill"></image>
</view>
<view class="divider-vertical"></view>
<view class="grid-item" @click="onCloseAlarm">
<text class="grid-label">{{ $i18n.t('disarmAlarm') }}</text>
<image style="width: 94rpx;height: 102rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uHzvT9I1TMOtJs93dpBP" mode="aspectFill"></image>
</view>
<view class="divider-horizontal"></view>
<view class="grid-item" @click="onNightMode">
<view class="grid-label">{{ $i18n.t('nightModeLabel') }}</view>
<image style="width: 102rpx;height: 102rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uYsM9Nn95AK9c3JkUqvb" mode="aspectFill"></image>
</view>
<view class="divider-vertical"></view>
<view class="grid-item" @click="onEmergency">
<view class="grid-label">{{ $i18n.t('emergencyLabel') }}</view>
<image style="width: 102rpx;height: 102rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uSkABcRy9Qk8KRzcFqj5" mode="aspectFill"></image>
</view>
</view>
<view class="" style="width: 100rpx;height: 100rpx;margin: auto;margin-top: 50rpx;" @click="btnkongjian">
<image style="width: 80rpx;height: 80rpx;" src="https://api.ccttiot.com/smartmeter/img/static/c650IT3ZxgcljMf8ZSGI" mode="aspectFill"></image>
</view>
<!-- 倒计时弹窗 -->
<view v-if="showCountdownModal" class="modal-overlay">
<view class="countdown-modal">
<view class="progress-circle">
<view class="progress-ring countdown-ring" :style="{ transform: `rotate(${(3-countdown)*120}deg)` }"></view>
<view class="countdown-number">{{ countdown }}</view>
</view>
<text class="modal-title">{{$i18n.t('fssos')}}...</text>
<button class="cancel-btn" @click="cancelCountdown">{{$i18n.t('cancel')}}</button>
</view>
</view>
<!-- 定位获取弹窗 -->
<view v-if="showLocationModal" class="modal-overlay">
<view class="location-modal">
<view class="progress-circle">
<view class="progress-ring location-ring" :style="{ transform: `rotate(${locationProgress*360}deg)` }"></view>
<view class="location-icon">
<view class="location-pin"></view>
<view class="location-waves">
<view class="wave wave1"></view>
<view class="wave wave2"></view>
<view class="wave wave3"></view>
</view>
</view>
</view>
<text class="modal-title">{{$i18n.t('fsdw')}}...</text>
<button class="cancel-btn" @click="cancelLocation">{{$i18n.t('cancel')}}</button>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ControlTab',
props: {
statusTitle: {
type: String,
default: ''
}
},
data() {
return {
kjobj:{},
titlename:'',
// 倒计时弹窗相关
showCountdownModal: false,
countdown: 3,
countdownTimer: null,
// 定位弹窗相关
showLocationModal: false,
locationTimer: null,
locationProgress: 0,
minHoldTimer: null
}
},
computed: {
// 计算属性可以在这里添加
},
created() {
this.getxq()
},
mounted() {
// 监听空间切换事件
uni.$on('spaceChanged', this.handleSpaceChanged)
},
beforeDestroy() {
// 移除事件监听
uni.$off('spaceChanged', this.handleSpaceChanged)
// 清理定时器
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
}
if (this.locationTimer) {
clearInterval(this.locationTimer)
}
if (this.minHoldTimer) {
clearTimeout(this.minHoldTimer)
this.minHoldTimer = null
}
},
methods: {
// 请求空间详情
getxq(){
let spaceId = uni.getStorageSync('kjid')
this.$http.get(`/bst/space/${spaceId}`).then(res => {
if(res.code == 200){
this.kjobj = res.data
if(res.data.status == 1){
this.titlename = this.$i18n.t('statusArmed')
}else if(res.data.status == 2){
this.titlename = this.$i18n.t('statusDisarmed')
}else if(res.data.status == 3){
this.titlename = this.$i18n.t('statusNight')
}
}
})
},
// 点击跳转到空间详情
btnkongjian(){
uni.navigateTo({
url:'/pages/kongjian/kongjianxq'
})
},
onOpenAlarm() {
uni.showModal({
title: this.$i18n.t('confirmOperation'),
content: this.$i18n.t('confirmArmAlarm'),
confirmText: this.$i18n.t('confirm'),
cancelText: this.$i18n.t('cancel'),
success: (res) => {
if (res.confirm) {
this.getstatus(1)
}
}
})
},
onCloseAlarm() {
uni.showModal({
title: this.$i18n.t('confirmOperation'),
content: this.$i18n.t('confirmDisarmAlarm'),
confirmText: this.$i18n.t('confirm'),
cancelText: this.$i18n.t('cancel'),
success: (res) => {
if (res.confirm) {
this.getstatus(2)
}
}
})
},
onNightMode() {
uni.showModal({
title: this.$i18n.t('confirmOperation'),
content: this.$i18n.t('confirmNightMode'),
confirmText: this.$i18n.t('confirm'),
cancelText: this.$i18n.t('cancel'),
success: (res) => {
if (res.confirm) {
this.getstatus(3)
}
}
})
},
// 操作状态
getstatus(status){
let spaceId = uni.getStorageSync('kjid')
let data = {
spaceId:spaceId,
status:status
}
this.$http.put(`/bst/space/changeStatus`,data).then(res => {
if(res.code == 200){
uni.showToast({ title: res.msg, icon: 'success',duration:3000})
this.getxq()
// 发射事件通知父组件状态已改变
this.$emit('status-changed', { status, spaceId })
}else{
uni.showToast({ title: res.msg, icon: 'none',duration:3000})
}
})
},
// 处理空间变化
handleSpaceChanged(payload){
try{
console.log('控制模块收到空间变化事件:', payload)
// 重新获取空间详情
this.getxq()
}catch(e){
console.warn('控制模块处理空间切换失败:', e)
}
},
// 点击报警
onEmergency() {
uni.showModal({
title: this.$i18n.t('confirmOperation'),
content: this.$i18n.t('confirmEmergency'),
confirmText: this.$i18n.t('confirm'),
cancelText: this.$i18n.t('cancel'),
success: (res) => {
if (res.confirm) {
this.startCountdown()
}
}
})
},
// 开始倒计时
startCountdown() {
this.showCountdownModal = true
this.countdown = 3
this.countdownTimer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
this.clearCountdown()
this.checkLocationPermission()
}
}, 1000)
},
// 取消倒计时
cancelCountdown() {
this.clearCountdown()
},
// 清除倒计时
clearCountdown() {
this.showCountdownModal = false
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
},
// 检查定位权限
checkLocationPermission() {
// 直接尝试获取定位,如果失败则发送无定位的请求
this.startLocation()
},
// 开始获取定位
startLocation() {
this.showLocationModal = true
this.locationProgress = 0
// 进度条动画(纯展示,不代表业务完成)
this.locationTimer = setInterval(() => {
this.locationProgress += 0.005
if (this.locationProgress >= 1) {
this.locationProgress = 0
}
}, 16) // 约60fps
// 至少展示3秒动画然后再继续后续流程
let holdDone = false
let locationSucceeded = false
let locationData = null
const proceedIfReady = () => {
if (!holdDone) return
this.clearLocation()
if (locationSucceeded && locationData) {
this.sendSOSRequest(locationData)
} else {
this.sendSOSDirectly()
}
}
this.minHoldTimer = setTimeout(() => {
holdDone = true
proceedIfReady()
}, 3000)
// 同时尝试获取位置结果先存起来等3秒到再决定是否带位置信息
if (typeof uni.getLocation === 'function') {
try {
uni.getLocation({
type: 'wgs84',
timeout: 5000,
success: (res) => {
console.log('获取位置成功:', res)
let spaceId = uni.getStorageSync('kjid')
locationData = {
spaceId: spaceId,
lon: res.longitude.toString(),
lat: res.latitude.toString()
}
locationSucceeded = true
proceedIfReady()
},
fail: (err) => {
console.log('获取位置失败:', err)
locationSucceeded = false
locationData = null
proceedIfReady()
}
})
} catch (error) {
console.log('调用定位接口异常:', error)
locationSucceeded = false
locationData = null
proceedIfReady()
}
} else {
console.log('当前环境不支持定位功能')
locationSucceeded = false
locationData = null
proceedIfReady()
}
},
// 取消定位
cancelLocation() {
this.clearLocation()
},
// 清除定位相关
clearLocation() {
this.showLocationModal = false
if (this.locationTimer) {
clearInterval(this.locationTimer)
this.locationTimer = null
}
this.locationProgress = 0
},
// 直接发送SOS请求无定位
sendSOSDirectly() {
let spaceId = uni.getStorageSync('kjid')
let data = {
spaceId: spaceId,
lon: '',
lat: ''
}
this.sendSOSRequest(data)
},
getsos(){
let spaceId = uni.getStorageSync('kjid')
// 获取当前位置
uni.getLocation({
type: 'wgs84', // 返回可以用于uni.openLocation的经纬度
timeout: 10000, // 10秒超时
success: (res) => {
uni.hideLoading()
console.log('获取位置成功:', res)
let data = {
spaceId: spaceId,
lon: res.longitude.toString(),
lat: res.latitude.toString()
}
this.sendSOSRequest(data)
},
fail: (err) => {
uni.hideLoading()
console.log('获取位置失败:', err)
// 如果获取位置失败,仍然发送请求但使用空坐标
let data = {
spaceId: spaceId,
lon: '',
lat: ''
}
this.sendSOSRequest(data)
}
})
},
// 发送SOS请求的方法
sendSOSRequest(data) {
this.$http.put(`/bst/space/panic`, data).then(res => {
if(res.code == 200){
uni.showToast({ title: res.msg, icon: 'success', duration: 3000})
}else{
uni.showToast({ title: res.msg, icon: 'none', duration: 3000})
}
}).catch(err => {
console.log('发送SOS请求失败:', err)
uni.showToast({ title: '发送报警失败,请重试', icon: 'none', duration: 3000})
})
},
},
}
</script>
<style lang="scss" scoped>
.control-tab{
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
background: #f5f6f7;
min-height: 100%;
padding-top: 50rpx;
}
.status-card{
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 28rpx;
}
.status-icon{
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 6rpx solid #3aa273;
border-right-color: transparent;
transform: rotate(30deg);
margin-bottom: 12rpx;
}
.status-title{
font-size: 32rpx;
color: #222;
font-weight: 600;
margin-bottom: 6rpx;
margin-top: 20rpx;
}
.status-subtitle{
font-size: 26rpx;
color: #35a06f;
}
.grid-card{
position: relative;
background: #fff;
margin-top: 100rpx;
border-radius: 24rpx;
padding: 28rpx 0;
display: grid;
grid-template-columns: 1fr 2rpx 1fr;
grid-template-rows: auto 2rpx auto;
width: 648rpx;
margin: auto;
}
.grid-item{
width: 324rpx;
height: 360rpx;
display: flex;
padding: 40rpx 60rpx;
box-sizing: border-box;
flex-direction: column;
align-items: center;
justify-content: center;
}
.grid-label{
font-size: 26rpx;
color: #7a7f85;
margin-bottom: 24rpx;
}
.grid-icon{
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: #000;
}
.icon-ring{
background: none;
border: 12rpx solid #000;
border-radius: 60rpx;
}
.icon-ring-small{
background: none;
border: 12rpx solid #000;
border-radius: 60rpx;
clip-path: inset(0 18rpx 0 0);
}
.icon-rotate{
background: none;
position: relative;
border: 12rpx solid #000;
border-radius: 60rpx;
}
.icon-rotate:after{
content: '';
position: absolute;
right: -8rpx;
bottom: -8rpx;
width: 28rpx;
height: 28rpx;
background: #000;
border-radius: 50%;
}
.icon-bang{
display: flex;
align-items: center;
justify-content: center;
background: #000;
color: #fff;
font-weight: 700;
}
.icon-bang:after{
content: '!';
color: #fff;
font-size: 64rpx;
line-height: 1;
}
.divider-vertical{
width: 2rpx;
background: #eee;
}
.divider-horizontal{
height: 2rpx;
background: #eee;
grid-column: 1 / 4;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
justify-content: flex-end;
z-index: 9999;
}
.countdown-modal, .location-modal {
background: #f5f6f7;
border-radius: 32rpx 32rpx 0 0;
padding: 80rpx 60rpx 80rpx;
width: 100%;
bottom: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
min-height: 60vh;
box-sizing: border-box;
animation: modal-slide-up 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes modal-slide-up {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.progress-circle {
position: relative;
width: 240rpx;
height: 240rpx;
margin-bottom: 60rpx;
}
.progress-ring {
position: absolute;
top: 0;
left: 0;
width: 240rpx;
height: 240rpx;
border-radius: 50%;
border: 12rpx solid #e8e9ea;
transition: transform 0.3s ease;
}
.countdown-ring {
border-top: 12rpx solid #ec5a5a;
border-right: 12rpx solid #ec5a5a;
border-bottom: 12rpx solid transparent;
border-left: 12rpx solid transparent;
transform-origin: center;
animation: location-spin 1.5s linear infinite;
transition: transform 0.1s ease-out;
}
.location-ring {
border-top: 12rpx solid #ec5a5a;
animation: location-spin 1.5s linear infinite;
transition: transform 0.1s ease-out;
}
@keyframes location-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.countdown-number {
position: absolute;
top: 50%;
left: 54%;
transform: translate(-50%, -50%);
font-size: 120rpx;
font-weight: bold;
color: #ec5a5a;
line-height: 1;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
animation: number-pulse 1s ease-in-out infinite;
}
@keyframes number-pulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
}
50% {
transform: translate(-50%, -50%) scale(1.05);
}
}
.location-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
}
.location-pin {
width: 20rpx;
height: 20rpx;
background: #ec5a5a;
border-radius: 50%;
position: absolute;
top: 50%;
left: 60%;
transform: translate(-50%, -50%);
z-index: 3;
box-shadow: 0 0 0 6rpx rgba(255, 107, 53, 0.3);
}
.location-pin::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32rpx;
height: 32rpx;
background: #ec5a5a;
border-radius: 50% 50% 50% 0;
transform: translate(-50%, -50%) rotate(-45deg);
}
.location-waves {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
}
.wave {
position: absolute;
top: 50%;
left: 60%;
transform: translate(-50%, -50%);
border: 2rpx solid #ec5a5a;
border-radius: 50%;
opacity: 0.6;
animation: wave-animation 2s ease-out infinite;
}
.wave1 {
width: 40rpx;
height: 40rpx;
animation-delay: 0s;
}
.wave2 {
width: 60rpx;
height: 60rpx;
animation-delay: 0.67s;
}
.wave3 {
width: 80rpx;
height: 80rpx;
animation-delay: 1.33s;
}
@keyframes wave-animation {
0% {
transform: translate(-50%, -50%) scale(0.3);
opacity: 1;
}
50% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.6;
}
100% {
transform: translate(-50%, -50%) scale(1.8);
opacity: 0;
}
}
.modal-title {
font-size: 36rpx;
color: #333;
margin-bottom: 80rpx;
text-align: center;
font-weight: 500;
}
.cancel-btn {
width: 100%;
max-width: 500rpx;
height: 100rpx;
background: #ec5a5a;
color: #fff;
border: none;
border-radius: 50rpx;
font-size: 36rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: auto;
position: relative;
overflow: hidden;
}
.cancel-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.cancel-btn:hover::before {
left: 100%;
}
.cancel-btn:active {
background: #e55a2b;
transform: translateY(2rpx) scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
}
</style>