feat 联调查询激活码接口

This commit is contained in:
Ironsp
2026-09-24 15:12:38 +08:00
parent 7eb6a626ad
commit c7694df2cc
4 changed files with 379 additions and 6 deletions

1
src/components.d.ts vendored
View File

@@ -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']

View 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>

View File

@@ -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,
} }
} }

View File

@@ -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>