feat 联调查询激活码接口
This commit is contained in:
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -11,6 +11,7 @@ declare module 'vue' {
|
|||||||
BluetoothConnect: typeof import('./components/bluetooth/connect.vue')['default']
|
BluetoothConnect: typeof import('./components/bluetooth/connect.vue')['default']
|
||||||
DeviceCard: typeof import('./components/deviceCard.vue')['default']
|
DeviceCard: typeof import('./components/deviceCard.vue')['default']
|
||||||
DeviceInfo: typeof import('./components/deviceInfo.vue')['default']
|
DeviceInfo: typeof import('./components/deviceInfo.vue')['default']
|
||||||
|
ExpireNotice: typeof import('./components/expireNotice.vue')['default']
|
||||||
FullscreenNav: typeof import('./components/FullscreenNav.vue')['default']
|
FullscreenNav: typeof import('./components/FullscreenNav.vue')['default']
|
||||||
HeatmapCanvas: typeof import('./components/heatmap/canvas.vue')['default']
|
HeatmapCanvas: typeof import('./components/heatmap/canvas.vue')['default']
|
||||||
HeatmapLayerPanel: typeof import('./components/heatmap/layerPanel.vue')['default']
|
HeatmapLayerPanel: typeof import('./components/heatmap/layerPanel.vue')['default']
|
||||||
|
|||||||
285
src/components/expireNotice.vue
Normal file
285
src/components/expireNotice.vue
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||||
|
|
||||||
|
const deviceAuth = useDeviceAuth()
|
||||||
|
const { authInfo, remainingDays, closeExpireNotice } = deviceAuth
|
||||||
|
|
||||||
|
/** 确保 App/H5 上 v-model 能正确读写弹窗显隐 */
|
||||||
|
const visible = computed({
|
||||||
|
get: () => deviceAuth.expireNoticeVisible.value,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
deviceAuth.expireNoticeVisible.value = value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const days = computed(() => {
|
||||||
|
const value = remainingDays.value
|
||||||
|
return value != null && value >= 0 ? value : 0
|
||||||
|
})
|
||||||
|
const deviceName = computed(() => authInfo.value?.deviceName || '--')
|
||||||
|
const deviceIdText = computed(() => authInfo.value?.deviceId || '--')
|
||||||
|
const endDateText = computed(() => formatEndDate(authInfo.value?.endDate))
|
||||||
|
|
||||||
|
function formatEndDate(value?: string) {
|
||||||
|
if (!value)
|
||||||
|
return '--'
|
||||||
|
const date = new Date(value.replace(/-/g, '/'))
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
if (/年/.test(value))
|
||||||
|
return value
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
closeExpireNotice()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<wd-popup
|
||||||
|
v-model="visible"
|
||||||
|
position="center"
|
||||||
|
close-on-click-modal
|
||||||
|
root-portal
|
||||||
|
:z-index="999"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<view class="expire-panel">
|
||||||
|
<view class="expire-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<wd-icon name="warning" size="22px" color="#F04848" />
|
||||||
|
<text class="header-title">
|
||||||
|
设备到期提醒
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-close" @tap.stop="handleClose">
|
||||||
|
<wd-icon name="close" size="20px" color="#999999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="expire-body">
|
||||||
|
<view class="warn-box">
|
||||||
|
<view class="warn-header">
|
||||||
|
<view class="lock">
|
||||||
|
<view class="lock-shackle" />
|
||||||
|
<view class="lock-body">
|
||||||
|
<view class="lock-dot" />
|
||||||
|
<view class="lock-slot" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="warn-main">
|
||||||
|
<text class="warn-title">
|
||||||
|
设备授权即将到期
|
||||||
|
</text>
|
||||||
|
<view class="days-badge">
|
||||||
|
<text class="days-text">
|
||||||
|
剩余{{ days }}天
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="warn-desc">
|
||||||
|
设备授权将于 {{ days }} 天后到期,到期后将锁定全部作业功能,请及时续期以免影响正常作业
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-name">
|
||||||
|
{{ deviceName }}
|
||||||
|
</text>
|
||||||
|
<text class="info-id">
|
||||||
|
设备ID: {{ deviceIdText }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="info-row info-row--expire">
|
||||||
|
<text class="expire-label">
|
||||||
|
授权到期日期
|
||||||
|
</text>
|
||||||
|
<text class="expire-value">
|
||||||
|
{{ endDateText }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</wd-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.expire-panel {
|
||||||
|
width: 560px;
|
||||||
|
max-width: 90vw;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expire-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-close {
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expire-body {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn-box {
|
||||||
|
background-color: #FFF1F0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-shackle {
|
||||||
|
width: 14px;
|
||||||
|
height: 10px;
|
||||||
|
border: 2px solid #F04848;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-body {
|
||||||
|
width: 20px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid #F04848;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-dot {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #F04848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-slot {
|
||||||
|
width: 1.5px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: #F04848;
|
||||||
|
border-radius: 1px;
|
||||||
|
margin-top: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #F04848;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-badge {
|
||||||
|
background-color: #FFE4E1;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #F04848;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #F04848;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row--expire {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-id {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expire-label,
|
||||||
|
.expire-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #F04848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expire-value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,7 +10,24 @@ const AUTH_INFO_KEY = 'device_auth_info'
|
|||||||
const ACTIVATION_CODE_KEY = 'device_activation_code'
|
const ACTIVATION_CODE_KEY = 'device_activation_code'
|
||||||
|
|
||||||
function readCachedAuthInfo(): DeviceAuthStatus | null {
|
function readCachedAuthInfo(): DeviceAuthStatus | null {
|
||||||
return storage.get<DeviceAuthStatus>(AUTH_INFO_KEY) ?? null
|
const cached = storage.get<DeviceAuthStatus>(AUTH_INFO_KEY)
|
||||||
|
if (cached && typeof cached === 'object')
|
||||||
|
return cached
|
||||||
|
|
||||||
|
// 兼容异常写入:直接读原始 storage
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(AUTH_INFO_KEY)
|
||||||
|
if (!raw)
|
||||||
|
return null
|
||||||
|
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||||
|
const value = parsed?.value ?? parsed
|
||||||
|
if (value && typeof value === 'object' && 'remainingDays' in value)
|
||||||
|
return value as DeviceAuthStatus
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function toAuthStatus(value: AuthStatus | string | number | null | undefined): AuthStatus | null {
|
function toAuthStatus(value: AuthStatus | string | number | null | undefined): AuthStatus | null {
|
||||||
@@ -58,7 +75,8 @@ function createUnauthorizedInfo(): DeviceAuthStatus {
|
|||||||
authStatus: '0',
|
authStatus: '0',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
remainingDays: 0,
|
// -1 表示无有效剩余天数,避免误触发到期提醒
|
||||||
|
remainingDays: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +85,9 @@ const authInfo = ref<DeviceAuthStatus | null>(readCachedAuthInfo())
|
|||||||
const activationCode = ref<string>(storage.get<string>(ACTIVATION_CODE_KEY, '') ?? '')
|
const activationCode = ref<string>(storage.get<string>(ACTIVATION_CODE_KEY, '') ?? '')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const authReady = ref(false)
|
const authReady = ref(false)
|
||||||
|
/** 到期提醒弹窗显示状态(App/H5 共用,避免页面 ref 调用失效) */
|
||||||
|
const expireNoticeVisible = ref(false)
|
||||||
|
let lastExpireShownAt = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 进入软件:先查本地激活码 → 无码再查授权状态并写入 storage
|
* 进入软件:先查本地激活码 → 无码再查授权状态并写入 storage
|
||||||
@@ -80,6 +101,49 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) {
|
|||||||
const needAuthPrompt = computed(() => authStatus.value === '0' || authStatus.value === '2')
|
const needAuthPrompt = computed(() => authStatus.value === '0' || authStatus.value === '2')
|
||||||
const hasActivationCode = computed(() => !!activationCode.value.trim())
|
const hasActivationCode = computed(() => !!activationCode.value.trim())
|
||||||
const deviceId = computed(() => authInfo.value?.deviceId || getDeviceId())
|
const deviceId = computed(() => authInfo.value?.deviceId || getDeviceId())
|
||||||
|
const remainingDays = computed(() => {
|
||||||
|
const days = authInfo.value?.remainingDays
|
||||||
|
return days == null || Number.isNaN(Number(days)) ? null : Number(days)
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 从当前授权信息判断是否需到期提醒:
|
||||||
|
* storage 中 remainingDays 为 0~7(含)时弹出
|
||||||
|
*/
|
||||||
|
const needExpireNotice = computed(() => {
|
||||||
|
const days = remainingDays.value
|
||||||
|
return days != null && days >= 0 && days <= 7
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 每次打开软件:先读 storage,再判断 remainingDays ≤ 7 */
|
||||||
|
function shouldShowExpireNoticeFromStorage(): boolean {
|
||||||
|
const cached = readCachedAuthInfo()
|
||||||
|
authInfo.value = cached
|
||||||
|
if (!cached)
|
||||||
|
return false
|
||||||
|
const days = Number(cached.remainingDays)
|
||||||
|
if (Number.isNaN(days))
|
||||||
|
return false
|
||||||
|
return days >= 0 && days <= 7
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开到期提醒(由首页 onShow/onMounted 调用) */
|
||||||
|
function openExpireNoticeIfNeeded() {
|
||||||
|
if (!shouldShowExpireNoticeFromStorage()) {
|
||||||
|
console.log('[expireNotice] skip, storage=', authInfo.value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastExpireShownAt < 1500)
|
||||||
|
return false
|
||||||
|
lastExpireShownAt = now
|
||||||
|
expireNoticeVisible.value = true
|
||||||
|
console.log('[expireNotice] open, remainingDays=', authInfo.value?.remainingDays)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeExpireNotice() {
|
||||||
|
expireNoticeVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
/** 将完整授权信息写入内存 + storage */
|
/** 将完整授权信息写入内存 + storage */
|
||||||
function saveAuthInfo(info: DeviceAuthStatus) {
|
function saveAuthInfo(info: DeviceAuthStatus) {
|
||||||
@@ -206,15 +270,21 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) {
|
|||||||
activationCode,
|
activationCode,
|
||||||
isActivated,
|
isActivated,
|
||||||
needAuthPrompt,
|
needAuthPrompt,
|
||||||
|
needExpireNotice,
|
||||||
hasActivationCode,
|
hasActivationCode,
|
||||||
deviceId,
|
deviceId,
|
||||||
|
remainingDays,
|
||||||
loading,
|
loading,
|
||||||
authReady,
|
authReady,
|
||||||
|
expireNoticeVisible,
|
||||||
saveAuthInfo,
|
saveAuthInfo,
|
||||||
setActivationCode,
|
setActivationCode,
|
||||||
fetchAuthStatus,
|
fetchAuthStatus,
|
||||||
initAuth,
|
initAuth,
|
||||||
activate,
|
activate,
|
||||||
refreshAuthStatus,
|
refreshAuthStatus,
|
||||||
|
shouldShowExpireNoticeFromStorage,
|
||||||
|
openExpireNoticeIfNeeded,
|
||||||
|
closeExpireNotice,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { nextTick, onMounted } from 'vue'
|
||||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
@@ -10,10 +11,24 @@ definePage({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { needAuthPrompt, initAuth } = useDeviceAuth({ watchShow: true })
|
const { needAuthPrompt, initAuth, openExpireNoticeIfNeeded } = useDeviceAuth({ watchShow: true })
|
||||||
|
|
||||||
onMounted(() => {
|
async function checkExpireNotice() {
|
||||||
initAuth()
|
await nextTick()
|
||||||
|
// App 端渲染稍慢,延迟再读 storage 弹窗
|
||||||
|
setTimeout(() => {
|
||||||
|
openExpireNoticeIfNeeded()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initAuth()
|
||||||
|
await checkExpireNotice()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 每次打开 / 回到首页:读 storage 判断是否弹到期提醒 */
|
||||||
|
onShow(() => {
|
||||||
|
checkExpireNotice()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -36,6 +51,8 @@ onMounted(() => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<expire-notice />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user