feat 联调

This commit is contained in:
Ironsp
2026-09-24 14:10:01 +08:00
parent 8a28f8654c
commit 7eb6a626ad
8 changed files with 186 additions and 39 deletions

View File

@@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDeviceAuth } from '@/composables/useDeviceAuth'
import { initTables } from '@/service' import { initTables } from '@/service'
const { initAuth } = useDeviceAuth()
onLaunch(async () => { onLaunch(async () => {
try { try {
await initTables() await initTables()
@@ -9,6 +12,13 @@ onLaunch(async () => {
catch (err) { catch (err) {
console.error('数据库初始化失败:', err) console.error('数据库初始化失败:', err)
} }
try {
await initAuth()
}
catch (err) {
console.error('设备授权检测失败:', err)
}
}) })
</script> </script>

View File

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

View File

@@ -7,7 +7,7 @@ import { useDbStore } from '@/store/modules/db'
const db = useDbStore() const db = useDbStore()
const { hasCurrentProject, currentProjectId } = useCurrentProject() const { hasCurrentProject, currentProjectId } = useCurrentProject()
const { isActivated } = useDeviceAuth() const { isActivated, needAuthPrompt } = useDeviceAuth()
const currentProject = computed(() => { const currentProject = computed(() => {
if (!hasCurrentProject.value || currentProjectId.value == null) if (!hasCurrentProject.value || currentProjectId.value == null)
@@ -99,7 +99,7 @@ onMounted(async () => {
</text> </text>
</view> </view>
<view class="card-body"> <view class="card-body">
<unauthorized v-if="!isActivated" /> <unauthorized v-if="needAuthPrompt" />
<view v-else class="progress-content"> <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 File

@@ -5,7 +5,7 @@ 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() const { isActivated, needAuthPrompt } = 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))
@@ -41,7 +41,7 @@ function goToDetail(item: Project) {
</wd-button> </wd-button>
</view> </view>
<view class="card-body"> <view class="card-body">
<unauthorized v-if="!isActivated" /> <unauthorized v-if="needAuthPrompt" />
<view v-else-if="!list.length" class="empty-tip"> <view v-else-if="!list.length" class="empty-tip">
暂无项目 暂无项目
</view> </view>

View File

@@ -4,7 +4,7 @@ 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 { needAuthPrompt } = useDeviceAuth()
const { speedText, tempText } = useRtkLive() const { speedText, tempText } = useRtkLive()
function goToWork() { function goToWork() {
@@ -22,7 +22,7 @@ function goToWork() {
</text> </text>
</view> </view>
<view class="card-body"> <view class="card-body">
<unauthorized v-if="!isActivated" /> <unauthorized v-if="needAuthPrompt" />
<view v-else-if="!hasCurrentProject" class="empty-tip"> <view v-else-if="!hasCurrentProject" class="empty-tip">
-- --
</view> </view>

View File

@@ -7,7 +7,7 @@ import { useDbStore } from '@/store/modules/db'
const db = useDbStore() const db = useDbStore()
const { hasCurrentProject, currentProjectId } = useCurrentProject() const { hasCurrentProject, currentProjectId } = useCurrentProject()
const { isActivated } = useDeviceAuth() const { isActivated, needAuthPrompt } = useDeviceAuth()
/** 首页只展示当前项目最近几条 */ /** 首页只展示当前项目最近几条 */
const list = computed(() => { const list = computed(() => {
@@ -50,7 +50,7 @@ function goToDetail(item: WarningRecord) {
</wd-button> </wd-button>
</view> </view>
<view class="card-body"> <view class="card-body">
<unauthorized v-if="!isActivated" /> <unauthorized v-if="needAuthPrompt" />
<view v-else-if="!hasCurrentProject" class="empty-tip"> <view v-else-if="!hasCurrentProject" class="empty-tip">
-- --
</view> </view>

View File

@@ -1,3 +1,4 @@
import type { DeviceAuthStatus } from '@/api/device'
import type { AuthStatus } from '@/api/types' import type { AuthStatus } from '@/api/types'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@@ -5,63 +6,192 @@ import { activateDevice, getDeviceStatus } from '@/api/device'
import { getDeviceId } from '@/utils/device' import { getDeviceId } from '@/utils/device'
import storage from '@/utils/storage' import storage from '@/utils/storage'
const AUTH_STATUS_KEY = 'device_auth_status' const AUTH_INFO_KEY = 'device_auth_info'
const ACTIVATION_CODE_KEY = 'device_activation_code'
/** 模块级共享,保证各页面读到同一授权状态 */ function readCachedAuthInfo(): DeviceAuthStatus | null {
const authStatus = ref<AuthStatus>(storage.get<AuthStatus>(AUTH_STATUS_KEY, '0') ?? '0') return storage.get<DeviceAuthStatus>(AUTH_INFO_KEY) ?? null
}
function toAuthStatus(value: AuthStatus | string | number | null | undefined): AuthStatus | null {
if (value == null || value === '')
return null
const status = String(value) as AuthStatus
if (status === '0' || status === '1' || status === '2' || status === '3')
return status
return null
}
/** 兼容 data 嵌套 / 扁平结构 / authStatus 为数字 */
function pickAuthPayload(res: unknown): Partial<DeviceAuthStatus> | null {
if (!res || typeof res !== 'object')
return null
const raw = res as Record<string, unknown>
const nested = raw.data
if (nested && typeof nested === 'object')
return nested as Partial<DeviceAuthStatus>
if ('authStatus' in raw)
return raw as Partial<DeviceAuthStatus>
return null
}
function normalizeAuthInfo(payload: Partial<DeviceAuthStatus> | null | undefined): DeviceAuthStatus | null {
if (!payload)
return null
const status = toAuthStatus(payload.authStatus as AuthStatus | string | number | undefined)
if (!status)
return null
return {
deviceId: payload.deviceId ?? getDeviceId(),
deviceName: payload.deviceName ?? '',
authStatus: status,
startDate: payload.startDate ?? '',
endDate: payload.endDate ?? '',
remainingDays: Number(payload.remainingDays ?? 0),
}
}
function createUnauthorizedInfo(): DeviceAuthStatus {
return {
deviceId: getDeviceId(),
deviceName: '',
authStatus: '0',
startDate: '',
endDate: '',
remainingDays: 0,
}
}
/** 模块级共享,保证各页面读到同一授权信息 */
const authInfo = ref<DeviceAuthStatus | null>(readCachedAuthInfo())
const activationCode = ref<string>(storage.get<string>(ACTIVATION_CODE_KEY, '') ?? '')
const loading = ref(false) const loading = ref(false)
const authReady = ref(false)
/** /**
* 设备授权状态:仅 authStatus === '1' 视为已激活 * 进入软件:先查本地激活码 → 无码再查授权状态并写入 storage
* 未激活时首页相关模块保留标题,内容展示未授权提示 * 激活成功后再查一次状态写入 storage,形成闭环
* * 仅未授权(0)或已过期(2)时展示 unauthorized / activateNotice
* 注意:onShow 仅在页面级调用时注册,避免子组件重复监听
*/ */
export function useDeviceAuth(options?: { watchShow?: boolean }) { export function useDeviceAuth(options?: { watchShow?: boolean }) {
const authStatus = computed(() => toAuthStatus(authInfo.value?.authStatus))
const isActivated = computed(() => authStatus.value === '1') const isActivated = computed(() => authStatus.value === '1')
const deviceId = computed(() => getDeviceId()) /** 是否需要展示未授权提示组件 */
const needAuthPrompt = computed(() => authStatus.value === '0' || authStatus.value === '2')
const hasActivationCode = computed(() => !!activationCode.value.trim())
const deviceId = computed(() => authInfo.value?.deviceId || getDeviceId())
function setAuthStatus(status: AuthStatus) { /** 将完整授权信息写入内存 + storage */
authStatus.value = status function saveAuthInfo(info: DeviceAuthStatus) {
storage.set(AUTH_STATUS_KEY, status) authInfo.value = info
storage.set(AUTH_INFO_KEY, info)
} }
function setActivationCode(code: string) {
const trimmed = code.trim()
activationCode.value = trimmed
if (trimmed)
storage.set(ACTIVATION_CODE_KEY, trimmed)
else
storage.remove(ACTIVATION_CODE_KEY)
}
/** 查询设备授权状态,并完整落库;失败时返回 null */
async function fetchAuthStatus() { async function fetchAuthStatus() {
loading.value = true loading.value = true
try { try {
const res = await getDeviceStatus(deviceId.value) const res = await getDeviceStatus(getDeviceId())
const status = res.data?.authStatus const info = normalizeAuthInfo(pickAuthPayload(res))
if (status != null && status !== '') if (info) {
setAuthStatus(String(status) as AuthStatus) saveAuthInfo(info)
return info
} }
catch { console.warn('[deviceAuth] 授权状态响应无法解析:', res)
// 接口失败时沿用本地缓存状态 return null
}
catch (err) {
console.warn('[deviceAuth] 查询授权状态失败:', err)
return null
} }
finally { finally {
loading.value = false loading.value = false
} }
} }
/** 提交授权码激活设备,成功后更新本地状态 */ /**
async function activate(activationCode: string) { * 启动时初始化授权:
const code = activationCode.trim() * 1. 本地已有激活码 → 读取 storage 中的授权信息
if (!code) * 2. 无激活码 → 查询设备授权状态;查不到则按未授权处理并展示提示
*/
async function initAuth() {
loading.value = true
try {
const localCode = storage.get<string>(ACTIVATION_CODE_KEY, '') ?? ''
if (localCode.trim()) {
activationCode.value = localCode.trim()
const cached = readCachedAuthInfo()
if (cached) {
authInfo.value = cached
return authInfo.value
}
// 有激活码但无缓存信息,补拉一次状态
const info = await fetchAuthStatus()
if (info)
return info
saveAuthInfo({ ...createUnauthorizedInfo(), authStatus: '1' })
return authInfo.value
}
// 无激活码:以服务端为准;失败或无法解析 → 未授权,展示提示组件
const info = await fetchAuthStatus()
if (info)
return info
saveAuthInfo(createUnauthorizedInfo())
return authInfo.value
}
finally {
loading.value = false
authReady.value = true
}
}
/**
* 提交授权码激活 → 缓存激活码 → 再查授权状态落库(闭环)
*/
async function activate(code: string) {
const trimmed = code.trim()
if (!trimmed)
throw new Error('请输入授权码') throw new Error('请输入授权码')
const res = await activateDevice({ const res = await activateDevice({
activationCode: code, activationCode: trimmed,
deviceId: deviceId.value, deviceId: getDeviceId(),
}) })
const ok = res.code === 200 || res.code === 0 const ok = res.code === 200 || res.code === 0
if (!ok) if (!ok)
throw new Error(res.msg || '激活失败') throw new Error(res.msg || '激活失败')
setAuthStatus('1') setActivationCode(trimmed)
const info = await fetchAuthStatus()
if (!info) {
saveAuthInfo({
deviceId: res.deviceId || getDeviceId(),
deviceName: authInfo.value?.deviceName ?? '',
authStatus: '1',
startDate: authInfo.value?.startDate ?? '',
endDate: res.endDate || authInfo.value?.endDate || '',
remainingDays: authInfo.value?.remainingDays ?? 0,
})
}
return res return res
} }
function refreshAuthStatus() { function refreshAuthStatus() {
authStatus.value = storage.get<AuthStatus>(AUTH_STATUS_KEY, '0') ?? '0' authInfo.value = readCachedAuthInfo()
activationCode.value = storage.get<string>(ACTIVATION_CODE_KEY, '') ?? ''
} }
if (options?.watchShow) { if (options?.watchShow) {
@@ -71,12 +201,19 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) {
} }
return { return {
authInfo,
authStatus, authStatus,
activationCode,
isActivated, isActivated,
needAuthPrompt,
hasActivationCode,
deviceId, deviceId,
loading, loading,
setAuthStatus, authReady,
saveAuthInfo,
setActivationCode,
fetchAuthStatus, fetchAuthStatus,
initAuth,
activate, activate,
refreshAuthStatus, refreshAuthStatus,
} }

View File

@@ -10,10 +10,10 @@ definePage({
}, },
}) })
const { isActivated, fetchAuthStatus } = useDeviceAuth({ watchShow: true }) const { needAuthPrompt, initAuth } = useDeviceAuth({ watchShow: true })
onMounted(() => { onMounted(() => {
fetchAuthStatus() initAuth()
}) })
</script> </script>
@@ -30,7 +30,7 @@ onMounted(() => {
<home-projectList /> <home-projectList />
</view> </view>
<view class="column-right"> <view class="column-right">
<home-activateNotice v-if="!isActivated" /> <home-activateNotice v-if="needAuthPrompt" />
<home-progress /> <home-progress />
<home-warning /> <home-warning />
</view> </view>