feat 授权激活
This commit is contained in:
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@@ -16,6 +16,7 @@ declare module 'vue' {
|
||||
HeatmapLayerPanel: typeof import('./components/heatmap/layerPanel.vue')['default']
|
||||
HeatmapLegend: typeof import('./components/heatmap/legend.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']
|
||||
HomeHomeNav: typeof import('./components/home/homeNav.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']
|
||||
SettingsWifiNetwork: typeof import('./components/settings/wifiNetwork.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']
|
||||
WarningItem: typeof import('./components/warning/item.vue')['default']
|
||||
WorkDashboard: typeof import('./components/work/dashboard.vue')['default']
|
||||
|
||||
155
src/components/home/activateNotice.vue
Normal file
155
src/components/home/activateNotice.vue
Normal 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>
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useCurrentProject } from '@/composables/useCurrentProject'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
|
||||
const { hasCurrentProject, currentProject } = useCurrentProject()
|
||||
const { isActivated } = useDeviceAuth()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,7 +14,8 @@ const { hasCurrentProject, currentProject } = useCurrentProject()
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="construction-info">
|
||||
<unauthorized v-if="!isActivated" />
|
||||
<view v-else class="construction-info">
|
||||
<view class="icon-placeholder" />
|
||||
<view class="info">
|
||||
<text class="segment">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useCurrentProject } from '@/composables/useCurrentProject'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
import { calcUncompactedArea } from '@/service/project'
|
||||
import { useDbStore } from '@/store/modules/db'
|
||||
|
||||
const db = useDbStore()
|
||||
const { hasCurrentProject, currentProjectId } = useCurrentProject()
|
||||
const { isActivated } = useDeviceAuth()
|
||||
|
||||
const currentProject = computed(() => {
|
||||
if (!hasCurrentProject.value || currentProjectId.value == null)
|
||||
@@ -92,12 +94,13 @@ onMounted(async () => {
|
||||
<text class="card-title">
|
||||
项目作业进度
|
||||
</text>
|
||||
<text class="card-sub">
|
||||
<text v-if="isActivated" class="card-sub">
|
||||
{{ currentProject?.stake_range || currentProject?.project_name || '暂无施工段' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="progress-content">
|
||||
<unauthorized v-if="!isActivated" />
|
||||
<view v-else class="progress-content">
|
||||
<view class="progress-circle">
|
||||
<wd-circle :model-value="progressPercent ?? 0" :size="120">
|
||||
<view class="progress-circle-text">
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { Project } from '@/service/project'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
import { useDbStore } from '@/store/modules/db'
|
||||
|
||||
const db = useDbStore()
|
||||
const { isActivated } = useDeviceAuth()
|
||||
|
||||
/** 首页展示最近 2 条,按 id 倒序 */
|
||||
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>
|
||||
<wd-button variant="text" @tap="goToProjectList">
|
||||
<wd-button v-if="isActivated" variant="text" @tap="goToProjectList">
|
||||
查看全部
|
||||
</wd-button>
|
||||
</view>
|
||||
<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>
|
||||
<template v-else>
|
||||
<home-list
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@tap="goToDetail(item)"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useCurrentProject } from '@/composables/useCurrentProject'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
import { useRtkLive } from '@/composables/useRtkLive'
|
||||
|
||||
const { hasCurrentProject } = useCurrentProject()
|
||||
const { isActivated } = useDeviceAuth()
|
||||
const { speedText, tempText } = useRtkLive()
|
||||
|
||||
function goToWork() {
|
||||
@@ -20,7 +22,8 @@ function goToWork() {
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view v-if="!hasCurrentProject" class="empty-tip">
|
||||
<unauthorized v-if="!isActivated" />
|
||||
<view v-else-if="!hasCurrentProject" class="empty-tip">
|
||||
--
|
||||
</view>
|
||||
<template v-else>
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import type { WarningRecord } from '@/service/warningRecord'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useCurrentProject } from '@/composables/useCurrentProject'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
import { useDbStore } from '@/store/modules/db'
|
||||
|
||||
const db = useDbStore()
|
||||
const { hasCurrentProject, currentProjectId } = useCurrentProject()
|
||||
const { isActivated } = useDeviceAuth()
|
||||
|
||||
/** 首页只展示当前项目最近几条 */
|
||||
const list = computed(() => {
|
||||
@@ -43,23 +45,26 @@ function goToDetail(item: WarningRecord) {
|
||||
<text class="card-title">
|
||||
异常警告
|
||||
</text>
|
||||
<wd-button variant="text" @tap="goToWarningList">
|
||||
<wd-button v-if="isActivated" variant="text" @tap="goToWarningList">
|
||||
查看全部
|
||||
</wd-button>
|
||||
</view>
|
||||
<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 v-else-if="!list.length" class="empty-tip">
|
||||
暂无异常警告
|
||||
</view>
|
||||
<template v-else>
|
||||
<home-warning_list
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@tap="goToDetail(item)"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
113
src/components/unauthorized.vue
Normal file
113
src/components/unauthorized.vue
Normal 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>
|
||||
83
src/composables/useDeviceAuth.ts
Normal file
83
src/composables/useDeviceAuth.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
"navigationBarTitleText": "Road-Roller"
|
||||
},
|
||||
"pages": [
|
||||
// GENERATED BY UNI-PAGES, PLATFORM: H5 || APP
|
||||
// GENERATED BY UNI-PAGES, PLATFORM: APP || H5
|
||||
{
|
||||
"path": "pages/Ahome/index",
|
||||
"type": "home",
|
||||
@@ -121,7 +121,7 @@
|
||||
"color": "@tabFontColor",
|
||||
"selectedColor": "@tabSelectedColor",
|
||||
"list": [
|
||||
// GENERATED BY UNI-PAGES, PLATFORM: H5 || APP
|
||||
// GENERATED BY UNI-PAGES, PLATFORM: APP || H5
|
||||
{
|
||||
"pagePath": "pages/Ahome/index",
|
||||
"text": "首页",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useDeviceAuth } from '@/composables/useDeviceAuth'
|
||||
|
||||
definePage({
|
||||
type: 'home',
|
||||
layout: false,
|
||||
@@ -6,6 +9,12 @@ definePage({
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { isActivated, fetchAuthStatus } = useDeviceAuth({ watchShow: true })
|
||||
|
||||
onMounted(() => {
|
||||
fetchAuthStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,6 +30,7 @@ definePage({
|
||||
<home-projectList />
|
||||
</view>
|
||||
<view class="column-right">
|
||||
<home-activateNotice v-if="!isActivated" />
|
||||
<home-progress />
|
||||
<home-warning />
|
||||
</view>
|
||||
|
||||
@@ -83,7 +83,7 @@ function generateUUID(): string {
|
||||
/**
|
||||
* 获取设备唯一标识
|
||||
*/
|
||||
function getDeviceId(): string {
|
||||
export function getDeviceId(): string {
|
||||
// 从存储获取
|
||||
const storedId = storage.get<string>(DEVICE_ID_KEY)
|
||||
if (storedId) {
|
||||
@@ -195,6 +195,7 @@ export function getOrInitDeviceInfo(): SystemInfo {
|
||||
|
||||
export default {
|
||||
getSystemInfo,
|
||||
getDeviceId,
|
||||
getDeviceTypeName,
|
||||
getOsTypeName,
|
||||
saveDeviceInfo,
|
||||
|
||||
Reference in New Issue
Block a user