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']
|
||||
DeviceCard: typeof import('./components/deviceCard.vue')['default']
|
||||
DeviceInfo: typeof import('./components/deviceInfo.vue')['default']
|
||||
ExpireNotice: typeof import('./components/expireNotice.vue')['default']
|
||||
FullscreenNav: typeof import('./components/FullscreenNav.vue')['default']
|
||||
HeatmapCanvas: typeof import('./components/heatmap/canvas.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'
|
||||
|
||||
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 {
|
||||
@@ -58,7 +75,8 @@ function createUnauthorizedInfo(): DeviceAuthStatus {
|
||||
authStatus: '0',
|
||||
startDate: '',
|
||||
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 loading = ref(false)
|
||||
const authReady = ref(false)
|
||||
/** 到期提醒弹窗显示状态(App/H5 共用,避免页面 ref 调用失效) */
|
||||
const expireNoticeVisible = ref(false)
|
||||
let lastExpireShownAt = 0
|
||||
|
||||
/**
|
||||
* 进入软件:先查本地激活码 → 无码再查授权状态并写入 storage
|
||||
@@ -80,6 +101,49 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) {
|
||||
const needAuthPrompt = computed(() => authStatus.value === '0' || authStatus.value === '2')
|
||||
const hasActivationCode = computed(() => !!activationCode.value.trim())
|
||||
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 */
|
||||
function saveAuthInfo(info: DeviceAuthStatus) {
|
||||
@@ -206,15 +270,21 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) {
|
||||
activationCode,
|
||||
isActivated,
|
||||
needAuthPrompt,
|
||||
needExpireNotice,
|
||||
hasActivationCode,
|
||||
deviceId,
|
||||
remainingDays,
|
||||
loading,
|
||||
authReady,
|
||||
expireNoticeVisible,
|
||||
saveAuthInfo,
|
||||
setActivationCode,
|
||||
fetchAuthStatus,
|
||||
initAuth,
|
||||
activate,
|
||||
refreshAuthStatus,
|
||||
shouldShowExpireNoticeFromStorage,
|
||||
openExpireNoticeIfNeeded,
|
||||
closeExpireNotice,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { nextTick, onMounted } from 'vue'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
|
||||
definePage({
|
||||
@@ -10,10 +11,24 @@ definePage({
|
||||
},
|
||||
})
|
||||
|
||||
const { needAuthPrompt, initAuth } = useDeviceAuth({ watchShow: true })
|
||||
const { needAuthPrompt, initAuth, openExpireNoticeIfNeeded } = useDeviceAuth({ watchShow: true })
|
||||
|
||||
onMounted(() => {
|
||||
initAuth()
|
||||
async function checkExpireNotice() {
|
||||
await nextTick()
|
||||
// App 端渲染稍慢,延迟再读 storage 弹窗
|
||||
setTimeout(() => {
|
||||
openExpireNoticeIfNeeded()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initAuth()
|
||||
await checkExpireNotice()
|
||||
})
|
||||
|
||||
/** 每次打开 / 回到首页:读 storage 判断是否弹到期提醒 */
|
||||
onShow(() => {
|
||||
checkExpireNotice()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -36,6 +51,8 @@ onMounted(() => {
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<expire-notice />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user