feat 授权激活

This commit is contained in:
Ironsp
2026-09-24 11:17:39 +08:00
parent 4ff3e92049
commit 5ab82d4571
12 changed files with 406 additions and 23 deletions

2
src/components.d.ts vendored
View File

@@ -16,6 +16,7 @@ declare module 'vue' {
HeatmapLayerPanel: typeof import('./components/heatmap/layerPanel.vue')['default'] HeatmapLayerPanel: typeof import('./components/heatmap/layerPanel.vue')['default']
HeatmapLegend: typeof import('./components/heatmap/legend.vue')['default'] HeatmapLegend: typeof import('./components/heatmap/legend.vue')['default']
HeatmapPointDetail: typeof import('./components/heatmap/pointDetail.vue')['default'] HeatmapPointDetail: typeof import('./components/heatmap/pointDetail.vue')['default']
HomeActivateNotice: typeof import('./components/home/activateNotice.vue')['default']
HomeConstruction: typeof import('./components/home/construction.vue')['default'] HomeConstruction: typeof import('./components/home/construction.vue')['default']
HomeHomeNav: typeof import('./components/home/homeNav.vue')['default'] HomeHomeNav: typeof import('./components/home/homeNav.vue')['default']
HomeList: typeof import('./components/home/list.vue')['default'] HomeList: typeof import('./components/home/list.vue')['default']
@@ -39,6 +40,7 @@ declare module 'vue' {
SettingsSensorConfig: typeof import('./components/settings/sensorConfig.vue')['default'] SettingsSensorConfig: typeof import('./components/settings/sensorConfig.vue')['default']
SettingsWifiNetwork: typeof import('./components/settings/wifiNetwork.vue')['default'] SettingsWifiNetwork: typeof import('./components/settings/wifiNetwork.vue')['default']
SettingsWifiSettings: typeof import('./components/settings/wifiSettings.vue')['default'] SettingsWifiSettings: typeof import('./components/settings/wifiSettings.vue')['default']
Unauthorized: typeof import('./components/unauthorized.vue')['default']
WarningFilter: typeof import('./components/warning/filter.vue')['default'] WarningFilter: typeof import('./components/warning/filter.vue')['default']
WarningItem: typeof import('./components/warning/item.vue')['default'] WarningItem: typeof import('./components/warning/item.vue')['default']
WorkDashboard: typeof import('./components/work/dashboard.vue')['default'] WorkDashboard: typeof import('./components/work/dashboard.vue')['default']

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
import { useDialog, useToast } from '@wot-ui/ui'
import { ref } from 'vue'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
const dialog = useDialog('activate-dialog')
const toast = useToast('activate-toast')
const { deviceId, activate } = useDeviceAuth()
const activationCode = ref('')
function goActivate() {
activationCode.value = ''
dialog
.show({
title: '设备授权激活',
showClose: true,
showCancelButton: false,
confirmButtonText: '激活',
closeOnClickModal: true,
beforeConfirm: async () => {
const code = activationCode.value.trim()
if (!code) {
toast.show('请输入授权码')
return false
}
try {
await activate(code)
toast.success('激活成功')
return true
}
catch (error: unknown) {
// Axios 错误已由 request 拦截器提示,此处只提示业务/校验错误
const isAxios = typeof error === 'object' && error !== null && 'config' in error
if (!isAxios && error instanceof Error)
toast.show(error.message)
return false
}
},
})
.catch(() => {})
}
</script>
<template>
<view class="card">
<text class="card-title">
设备未授权激活
</text>
<view class="notice-box">
<text class="notice-title">
检测到 1 台新设备待激活
</text>
<text class="notice-desc">
请输入设备ID完成授权激活后,方可获取该设备ID获取压实数据及位置信息
</text>
</view>
<view class="card-footer">
<wd-button type="primary" size="large" round @tap="goActivate">
立即激活
</wd-button>
</view>
<wd-dialog selector="activate-dialog">
<view class="activate-body">
<text class="device-label">
设备ID
</text>
<view class="device-id-box">
<text class="device-id-text">
{{ deviceId }}
</text>
</view>
<wd-input
v-model="activationCode"
prefix-icon="lock"
clearable
/>
</view>
</wd-dialog>
<wd-toast selector="activate-toast" />
</view>
</template>
<style scoped>
.card {
background-color: #ffffff;
border-radius: 12px;
margin-bottom: 12px;
padding: 16px;
overflow: hidden;
}
.card-title {
display: block;
font-size: 22px;
font-weight: bold;
color: #333333;
margin-bottom: 12px;
}
.notice-box {
background-color: #E8F3FF;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.notice-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #2B6DE5;
margin-bottom: 8px;
line-height: 24px;
}
.notice-desc {
display: block;
font-size: 14px;
color: #5B8DEF;
line-height: 22px;
}
.card-footer {
display: flex;
justify-content: flex-end;
}
.activate-body {
text-align: left;
}
.device-label {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 8px;
}
.device-id-box {
background-color: #f5f5f5;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 24px;
}
.device-id-text {
font-size: 16px;
font-weight: bold;
color: #333333;
word-break: break-all;
}
</style>

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentProject } from '@/composables/useCurrentProject' import { useCurrentProject } from '@/composables/useCurrentProject'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
const { hasCurrentProject, currentProject } = useCurrentProject() const { hasCurrentProject, currentProject } = useCurrentProject()
const { isActivated } = useDeviceAuth()
</script> </script>
<template> <template>
@@ -12,7 +14,8 @@ const { hasCurrentProject, currentProject } = useCurrentProject()
</text> </text>
</view> </view>
<view class="card-body"> <view class="card-body">
<view class="construction-info"> <unauthorized v-if="!isActivated" />
<view v-else class="construction-info">
<view class="icon-placeholder" /> <view class="icon-placeholder" />
<view class="info"> <view class="info">
<text class="segment"> <text class="segment">

View File

@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useCurrentProject } from '@/composables/useCurrentProject' import { useCurrentProject } from '@/composables/useCurrentProject'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
import { calcUncompactedArea } from '@/service/project' import { calcUncompactedArea } from '@/service/project'
import { useDbStore } from '@/store/modules/db' import { useDbStore } from '@/store/modules/db'
const db = useDbStore() const db = useDbStore()
const { hasCurrentProject, currentProjectId } = useCurrentProject() const { hasCurrentProject, currentProjectId } = useCurrentProject()
const { isActivated } = useDeviceAuth()
const currentProject = computed(() => { const currentProject = computed(() => {
if (!hasCurrentProject.value || currentProjectId.value == null) if (!hasCurrentProject.value || currentProjectId.value == null)
@@ -92,12 +94,13 @@ onMounted(async () => {
<text class="card-title"> <text class="card-title">
项目作业进度 项目作业进度
</text> </text>
<text class="card-sub"> <text v-if="isActivated" class="card-sub">
{{ currentProject?.stake_range || currentProject?.project_name || '暂无施工段' }} {{ currentProject?.stake_range || currentProject?.project_name || '暂无施工段' }}
</text> </text>
</view> </view>
<view class="card-body"> <view class="card-body">
<view class="progress-content"> <unauthorized v-if="!isActivated" />
<view v-else class="progress-content">
<view class="progress-circle"> <view class="progress-circle">
<wd-circle :model-value="progressPercent ?? 0" :size="120"> <wd-circle :model-value="progressPercent ?? 0" :size="120">
<view class="progress-circle-text"> <view class="progress-circle-text">

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Project } from '@/service/project' import type { Project } from '@/service/project'
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
import { useDbStore } from '@/store/modules/db' import { useDbStore } from '@/store/modules/db'
const db = useDbStore() const db = useDbStore()
const { isActivated } = useDeviceAuth()
/** 首页展示最近 2 条,按 id 倒序 */ /** 首页展示最近 2 条,按 id 倒序 */
const list = computed(() => (db.project || []).slice().sort((a, b) => (b.id ?? 0) - (a.id ?? 0)).slice(0, 2)) const list = computed(() => (db.project || []).slice().sort((a, b) => (b.id ?? 0) - (a.id ?? 0)).slice(0, 2))
@@ -34,20 +36,23 @@ function goToDetail(item: Project) {
<text class="card-title"> <text class="card-title">
项目列表 项目列表
</text> </text>
<wd-button variant="text" @tap="goToProjectList"> <wd-button v-if="isActivated" variant="text" @tap="goToProjectList">
查看全部 查看全部
</wd-button> </wd-button>
</view> </view>
<view class="card-body"> <view class="card-body">
<view v-if="!list.length" class="empty-tip"> <unauthorized v-if="!isActivated" />
<view v-else-if="!list.length" class="empty-tip">
暂无项目 暂无项目
</view> </view>
<template v-else>
<home-list <home-list
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
:item="item" :item="item"
@tap="goToDetail(item)" @tap="goToDetail(item)"
/> />
</template>
</view> </view>
</view> </view>
</template> </template>

View File

@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentProject } from '@/composables/useCurrentProject' import { useCurrentProject } from '@/composables/useCurrentProject'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
import { useRtkLive } from '@/composables/useRtkLive' import { useRtkLive } from '@/composables/useRtkLive'
const { hasCurrentProject } = useCurrentProject() const { hasCurrentProject } = useCurrentProject()
const { isActivated } = useDeviceAuth()
const { speedText, tempText } = useRtkLive() const { speedText, tempText } = useRtkLive()
function goToWork() { function goToWork() {
@@ -20,7 +22,8 @@ function goToWork() {
</text> </text>
</view> </view>
<view class="card-body"> <view class="card-body">
<view v-if="!hasCurrentProject" class="empty-tip"> <unauthorized v-if="!isActivated" />
<view v-else-if="!hasCurrentProject" class="empty-tip">
-- --
</view> </view>
<template v-else> <template v-else>

View File

@@ -2,10 +2,12 @@
import type { WarningRecord } from '@/service/warningRecord' import type { WarningRecord } from '@/service/warningRecord'
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useCurrentProject } from '@/composables/useCurrentProject' import { useCurrentProject } from '@/composables/useCurrentProject'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
import { useDbStore } from '@/store/modules/db' import { useDbStore } from '@/store/modules/db'
const db = useDbStore() const db = useDbStore()
const { hasCurrentProject, currentProjectId } = useCurrentProject() const { hasCurrentProject, currentProjectId } = useCurrentProject()
const { isActivated } = useDeviceAuth()
/** 首页只展示当前项目最近几条 */ /** 首页只展示当前项目最近几条 */
const list = computed(() => { const list = computed(() => {
@@ -43,23 +45,26 @@ function goToDetail(item: WarningRecord) {
<text class="card-title"> <text class="card-title">
异常警告 异常警告
</text> </text>
<wd-button variant="text" @tap="goToWarningList"> <wd-button v-if="isActivated" variant="text" @tap="goToWarningList">
查看全部 查看全部
</wd-button> </wd-button>
</view> </view>
<view class="card-body"> <view class="card-body">
<view v-if="!hasCurrentProject" class="empty-tip"> <unauthorized v-if="!isActivated" />
<view v-else-if="!hasCurrentProject" class="empty-tip">
-- --
</view> </view>
<view v-else-if="!list.length" class="empty-tip"> <view v-else-if="!list.length" class="empty-tip">
暂无异常警告 暂无异常警告
</view> </view>
<template v-else>
<home-warning_list <home-warning_list
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.id"
:item="item" :item="item"
@tap="goToDetail(item)" @tap="goToDetail(item)"
/> />
</template>
</view> </view>
</view> </view>
</template> </template>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
withDefaults(defineProps<{
title?: string
tipLine1?: string
tipLine2?: string
}>(), {
title: '设备未授权激活',
tipLine1: '请点击右上角设置图标',
tipLine2: '复制设备 ID 获取授权码完成激活',
})
</script>
<template>
<view class="unauthorized">
<view class="icon-wrap">
<view class="lock">
<view class="lock-shackle" />
<view class="lock-body">
<view class="lock-dot" />
<view class="lock-slot" />
</view>
</view>
</view>
<text class="title">
{{ title }}
</text>
<text class="tip">
{{ tipLine1 }}
</text>
<text class="tip">
{{ tipLine2 }}
</text>
</view>
</template>
<style scoped>
.unauthorized {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
}
.icon-wrap {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #FFECEC;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.lock {
display: flex;
flex-direction: column;
align-items: center;
}
.lock-shackle {
width: 22px;
height: 16px;
border: 3px solid #F04848;
border-bottom: none;
border-radius: 11px 11px 0 0;
box-sizing: border-box;
}
.lock-body {
width: 32px;
height: 24px;
border: 3px solid #F04848;
border-radius: 4px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: -2px;
}
.lock-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #F04848;
}
.lock-slot {
width: 2px;
height: 5px;
background-color: #F04848;
border-radius: 1px;
margin-top: -1px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #333333;
margin-bottom: 12px;
text-align: center;
}
.tip {
font-size: 14px;
color: #999999;
line-height: 22px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,83 @@
import type { AuthStatus } from '@/api/types'
import { onShow } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
import { activateDevice, getDeviceStatus } from '@/api/device'
import { getDeviceId } from '@/utils/device'
import storage from '@/utils/storage'
const AUTH_STATUS_KEY = 'device_auth_status'
/** 模块级共享,保证各页面读到同一授权状态 */
const authStatus = ref<AuthStatus>(storage.get<AuthStatus>(AUTH_STATUS_KEY, '0') ?? '0')
const loading = ref(false)
/**
* 设备授权状态:仅 authStatus === '1' 视为已激活
* 未激活时首页相关模块保留标题,内容展示未授权提示
*
* 注意:onShow 仅在页面级调用时注册,避免子组件重复监听
*/
export function useDeviceAuth(options?: { watchShow?: boolean }) {
const isActivated = computed(() => authStatus.value === '1')
const deviceId = computed(() => getDeviceId())
function setAuthStatus(status: AuthStatus) {
authStatus.value = status
storage.set(AUTH_STATUS_KEY, status)
}
async function fetchAuthStatus() {
loading.value = true
try {
const res = await getDeviceStatus(deviceId.value)
const status = res.data?.authStatus
if (status != null && status !== '')
setAuthStatus(String(status) as AuthStatus)
}
catch {
// 接口失败时沿用本地缓存状态
}
finally {
loading.value = false
}
}
/** 提交授权码激活设备,成功后更新本地状态 */
async function activate(activationCode: string) {
const code = activationCode.trim()
if (!code)
throw new Error('请输入授权码')
const res = await activateDevice({
activationCode: code,
deviceId: deviceId.value,
})
const ok = res.code === 200 || res.code === 0
if (!ok)
throw new Error(res.msg || '激活失败')
setAuthStatus('1')
return res
}
function refreshAuthStatus() {
authStatus.value = storage.get<AuthStatus>(AUTH_STATUS_KEY, '0') ?? '0'
}
if (options?.watchShow) {
onShow(() => {
refreshAuthStatus()
})
}
return {
authStatus,
isActivated,
deviceId,
loading,
setAuthStatus,
fetchAuthStatus,
activate,
refreshAuthStatus,
}
}

View File

@@ -16,7 +16,7 @@
"navigationBarTitleText": "Road-Roller" "navigationBarTitleText": "Road-Roller"
}, },
"pages": [ "pages": [
// GENERATED BY UNI-PAGES, PLATFORM: H5 || APP // GENERATED BY UNI-PAGES, PLATFORM: APP || H5
{ {
"path": "pages/Ahome/index", "path": "pages/Ahome/index",
"type": "home", "type": "home",
@@ -121,7 +121,7 @@
"color": "@tabFontColor", "color": "@tabFontColor",
"selectedColor": "@tabSelectedColor", "selectedColor": "@tabSelectedColor",
"list": [ "list": [
// GENERATED BY UNI-PAGES, PLATFORM: H5 || APP // GENERATED BY UNI-PAGES, PLATFORM: APP || H5
{ {
"pagePath": "pages/Ahome/index", "pagePath": "pages/Ahome/index",
"text": "首页", "text": "首页",

View File

@@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'
import { useDeviceAuth } from '@/composables/useDeviceAuth'
definePage({ definePage({
type: 'home', type: 'home',
layout: false, layout: false,
@@ -6,6 +9,12 @@ definePage({
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
const { isActivated, fetchAuthStatus } = useDeviceAuth({ watchShow: true })
onMounted(() => {
fetchAuthStatus()
})
</script> </script>
<template> <template>
@@ -21,6 +30,7 @@ definePage({
<home-projectList /> <home-projectList />
</view> </view>
<view class="column-right"> <view class="column-right">
<home-activateNotice v-if="!isActivated" />
<home-progress /> <home-progress />
<home-warning /> <home-warning />
</view> </view>

View File

@@ -83,7 +83,7 @@ function generateUUID(): string {
/** /**
* 获取设备唯一标识 * 获取设备唯一标识
*/ */
function getDeviceId(): string { export function getDeviceId(): string {
// 从存储获取 // 从存储获取
const storedId = storage.get<string>(DEVICE_ID_KEY) const storedId = storage.get<string>(DEVICE_ID_KEY)
if (storedId) { if (storedId) {
@@ -195,6 +195,7 @@ export function getOrInitDeviceInfo(): SystemInfo {
export default { export default {
getSystemInfo, getSystemInfo,
getDeviceId,
getDeviceTypeName, getDeviceTypeName,
getOsTypeName, getOsTypeName,
saveDeviceInfo, saveDeviceInfo,