1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-08 13:49:37 +00:00

feat: 实现渐进式设备注册功能,自动生成设备信息并获取访问令牌,优化用户体验

This commit is contained in:
Sunwuyuan 2025-11-09 14:30:09 +08:00
parent 670666aa41
commit 49ea5f1b2f
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
5 changed files with 902 additions and 254 deletions

View File

@ -17,16 +17,16 @@ axiosInstance.interceptors.request.use(
// 只有在 kv-server 或 classworkscloud 模式下才添加请求头 // 只有在 kv-server 或 classworkscloud 模式下才添加请求头
if (provider === "kv-server" || provider === "classworkscloud") { if (provider === "kv-server" || provider === "classworkscloud") {
// 确保每次请求时都获取最新的 siteKey // 优先使用新的 kvToken
const kvToken = getSetting("server.kvToken");
if (kvToken) {
requestConfig.headers["x-app-token"] = kvToken;
} else {
// 向后兼容旧的 siteKey
const siteKey = getSetting("server.siteKey"); const siteKey = getSetting("server.siteKey");
if (siteKey) { if (siteKey) {
requestConfig.headers["x-site-key"] = Base64.encode(siteKey); requestConfig.headers["x-site-key"] = Base64.encode(siteKey);
} }
// 自动添加命名空间密码
const namespacePassword = getSetting("namespace.password");
if (namespacePassword) {
requestConfig.headers["x-namespace-password"] = Base64.encode(namespacePassword);
} }
} }

View File

@ -99,6 +99,7 @@
</v-card-item> </v-card-item>
</v-card> </v-card>
</div> </div>
<div class="options-buttons"> <div class="options-buttons">
<v-btn <v-btn
variant="tonal" variant="tonal"
@ -145,7 +146,10 @@
v-model="showGuideDialog" v-model="showGuideDialog"
max-width="600" max-width="600"
> >
<FirstTimeGuide @close="showGuideDialog = false" /> <FirstTimeGuide
@close="showGuideDialog = false"
@success="handleGuideSuccess"
/>
</v-dialog> </v-dialog>
<v-dialog <v-dialog
@ -275,6 +279,13 @@ const handleAutoAuthorize = () => {
window.location.href = url window.location.href = url
} }
const handleGuideSuccess = (tokenData) => {
showGuideDialog.value = false
console.log('渐进式注册成功:', tokenData)
evaluateVisibility()
emit('done')
}
const handleAuthSuccess = (tokenData) => { const handleAuthSuccess = (tokenData) => {
showDeviceAuthDialog.value = false showDeviceAuthDialog.value = false
console.log('认证成功:', tokenData) console.log('认证成功:', tokenData)

View File

@ -140,141 +140,7 @@
</div> </div>
</v-card> </v-card>
<!-- 工作流程说明 -->
<v-card
variant="outlined"
class="pa-5 mb-4"
>
<h4 class="text-h6 mb-4">
<v-icon
color="primary"
class="mr-2"
>
mdi-information
</v-icon>
工作流程
</h4>
<v-timeline
side="end"
density="compact"
line-thickness="2"
>
<v-timeline-item
dot-color="primary"
size="small"
>
<div class="text-body-2">
<strong>步骤 1:</strong> Classworks 应用中编辑作业
</div>
</v-timeline-item>
<v-timeline-item
dot-color="success"
size="small"
>
<div class="text-body-2">
<strong>步骤 2:</strong> 数据自动上传到 Classworks KV
</div>
</v-timeline-item>
<v-timeline-item
dot-color="info"
size="small"
>
<div class="text-body-2">
<strong>步骤 3:</strong> 其他设备从 Classworks KV 同步数据
</div>
</v-timeline-item>
<v-timeline-item
dot-color="warning"
size="small"
>
<div class="text-body-2">
<strong>步骤 4:</strong> 所有设备显示相同的作业内容
</div>
</v-timeline-item>
</v-timeline>
</v-card>
<!-- 优势说明 -->
<v-row>
<v-col
cols="12"
md="4"
>
<v-card
variant="tonal"
color="blue"
class="pa-4 h-100"
>
<v-icon
size="40"
color="blue-darken-2"
class="mb-2"
>
mdi-devices
</v-icon>
<h5 class="text-subtitle-1 font-weight-bold mb-2">
多设备访问
</h5>
<p class="text-body-2">
在教室办公室家中的任何设备上访问相同的数据
</p>
</v-card>
</v-col>
<v-col
cols="12"
md="4"
>
<v-card
variant="tonal"
color="green"
class="pa-4 h-100"
>
<v-icon
size="40"
color="green-darken-2"
class="mb-2"
>
mdi-sync
</v-icon>
<h5 class="text-subtitle-1 font-weight-bold mb-2">
实时同步
</h5>
<p class="text-body-2">
修改后立即同步所有设备保持数据一致
</p>
</v-card>
</v-col>
<v-col
cols="12"
md="4"
>
<v-card
variant="tonal"
color="orange"
class="pa-4 h-100"
>
<v-icon
size="40"
color="orange-darken-2"
class="mb-2"
>
mdi-shield-lock
</v-icon>
<h5 class="text-subtitle-1 font-weight-bold mb-2">
安全可靠
</h5>
<p class="text-body-2">
通过密码和命名空间隔离保护班级数据安全
</p>
</v-card>
</v-col>
</v-row>
</div> </div>
<!-- 步骤 3: 询问使用场景 --> <!-- 步骤 3: 询问使用场景 -->
@ -386,6 +252,32 @@
</h3> </h3>
</div> </div>
<v-card
variant="tonal"
class="pa-6 mb-6"
>
<div class="d-flex flex-column flex-sm-row align-center">
<div class="flex-grow-1">
<h4 class="text-h6 font-weight-bold mb-2">
自动注册设备
</h4>
<p class="text-body-2 mb-3 text-medium-emphasis">
通过引导式流程自动创建设备获取令牌并完成初始化适合首次体验或快速部署多终端
</p>
<v-btn
color="primary"
size="large"
variant="elevated"
prepend-icon="mdi-flash"
@click="goToProgressiveStep"
>
自动注册
</v-btn>
</div>
</div>
</v-card>
<div class="mb-6">
也可以手动前往 Classworks KV 控制台获取认证信息</div>
<v-card <v-card
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'" :variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
:color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' " :color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' "
@ -398,6 +290,7 @@
> >
mdi-open-in-new mdi-open-in-new
</v-icon> </v-icon>
<h4 class="text-h6 font-weight-bold"> <h4 class="text-h6 font-weight-bold">
请访问 {{ kvserverurl=='https://kv.houlang.cloud'? 'Classworks KV' : '自定义的 Classworks KV 实例 ' }} 控制台 请访问 {{ kvserverurl=='https://kv.houlang.cloud'? 'Classworks KV' : '自定义的 Classworks KV 实例 ' }} 控制台
</h4> </h4>
@ -409,33 +302,6 @@
</h6> </h6>
</v-card> </v-card>
<v-card
variant="tonal"
color="info"
class="pa-5"
>
<div class="text-body-1 mb-3">
<v-icon
size="20"
class="mr-2"
>
mdi-information
</v-icon>
在控制台完成以下操作
</div>
<div class="text-body-2 mb-2">
1. 注册或登录账号
</div>
<div class="text-body-2 mb-2">
2. 创建班级空间
</div>
<div class="text-body-2 mb-2">
3. 获取命名空间和密码
</div>
<div class="text-body-2">
4. 返回这里输入认证信息
</div>
</v-card>
<!-- 常见问题 --> <!-- 常见问题 -->
<v-expansion-panels <v-expansion-panels
@ -511,6 +377,166 @@
</v-expansion-panel> </v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
</div> </div>
<!-- 步骤 5: 渐进式注册完整流程 -->
<div
v-show="currentStep === 5"
class="step-content"
>
<div class="text-center mb-6">
<v-avatar
size="80"
color="primary"
variant="tonal"
class="mb-4"
>
<v-icon size="48">
mdi-rocket-launch
</v-icon>
</v-avatar>
<h3 class="text-h5 font-weight-bold mb-2">
渐进式注册
</h3>
<p class="text-body-2 text-medium-emphasis">
您可以暂时不配置 Classworks KV
</p>
</div>
<v-progress-linear
:model-value="progressValue"
height="8"
color="primary"
rounded
class="mb-6"
/>
<v-row>
<v-col
cols="12"
>
<v-card
variant="tonal"
:color="statusColor"
>
<v-card-item>
<div class="d-flex align-center mb-3">
<v-icon
:color="statusColor"
class="mr-2"
size="32"
>
{{ statusIcon }}
</v-icon>
<div class="text-h6 font-weight-medium">
{{ statusTitle }}
</div>
</div>
<div
v-if="deviceInfo"
class="text-body-2 mb-2"
>
<div class="mb-2">
<strong>设备名称</strong>{{ deviceInfo.deviceName }}
</div>
<div>
<strong>设备 UUID</strong>
<code class="device-code">{{ deviceInfo.uuid }}</code>
</div>
</div>
<div
v-if="progressiveStatus === 'error'"
class="text-body-2 text-error"
>
{{ progressiveError }}
</div>
</v-card-item>
</v-card>
</v-col>
<v-col
cols="12"
>
<v-card variant="outlined">
<v-card-item>
<div class="text-subtitle-2 font-weight-medium mb-3">
过程日志
</div>
<div class="log-box">
<div
v-for="(log, i) in logs"
:key="i"
class="text-caption log-line"
>
{{ log.time }} · {{ log.message }}
</div>
<div
v-if="!logs.length"
class="text-caption text-medium-emphasis"
>
等待开始
</div>
</div>
</v-card-item>
</v-card>
</v-col>
</v-row>
<div class="d-flex flex-wrap gap-2 mt-4">
<v-btn
v-if="progressiveStatus === 'idle'"
color="primary"
size="large"
prepend-icon="mdi-play"
@click="startProgressiveRegister"
>
开始创建
</v-btn>
<v-btn
v-if="progressiveStatus === 'error'"
color="error"
variant="outlined"
prepend-icon="mdi-refresh"
@click="retryProgressiveRegister"
>
重试
</v-btn>
<v-btn
v-if="progressiveStatus === 'registering'"
color="primary"
variant="tonal"
:loading="true"
prepend-icon="mdi-progress-clock"
>
正在执行
</v-btn>
<v-btn
v-if="progressiveStatus === 'success'"
color="success"
size="large"
variant="elevated"
prepend-icon="mdi-check-circle"
@click="applyTokenAndClose"
>
应用令牌并关闭
</v-btn>
<v-btn
v-if="progressiveStatus === 'success'"
color="primary"
size="large"
variant="outlined"
prepend-icon="mdi-open-in-new"
@click="openAuthPage"
>
前往绑定账户
</v-btn>
</div>
</div>
</v-card-text> </v-card-text>
<!-- 底部操作按钮 --> <!-- 底部操作按钮 -->
@ -528,7 +554,7 @@
</v-btn> </v-btn>
<v-spacer /> <v-spacer />
<v-btn <v-btn
v-if="currentStep < 4" v-if="currentStep < totalSteps && currentStep !== 4"
:disabled="currentStep === 3 && !storageType" :disabled="currentStep === 3 && !storageType"
size="large" size="large"
color="primary" color="primary"
@ -541,7 +567,7 @@
</v-icon> </v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
v-else v-if="currentStep === totalSteps || currentStep === 4"
size="large" size="large"
color="primary" color="primary"
variant="elevated" variant="elevated"
@ -554,14 +580,24 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed } from 'vue'
import { getSetting } from '@/utils/settings' import { getSetting, setSetting } from '@/utils/settings'
const emit = defineEmits(['close']) import axios from '@/axios/axios'
import { v4 as uuidv4 } from 'uuid'
const emit = defineEmits(['close', 'success'])
const kvserverurl = getSetting('server.authDomain') const kvserverurl = getSetting('server.authDomain')
const currentStep = ref(1) const currentStep = ref(1)
const totalSteps = 4 const totalSteps = 5
const storageType = ref('') const storageType = ref('')
//
const progressiveStatus = ref('idle') // 'idle' | 'registering' | 'success' | 'error'
const progressiveError = ref('')
const deviceInfo = ref(null)
const tokenData = ref(null) // token
const logs = ref([])
const stepStates = ref({ 1: false, 2: false, 3: false, 4: false })
const nextStep = () => { const nextStep = () => {
if (currentStep.value < totalSteps) { if (currentStep.value < totalSteps) {
currentStep.value++ currentStep.value++
@ -586,6 +622,147 @@ const finish = () => {
const openKVSite = () => { const openKVSite = () => {
window.open(kvserverurl, '_blank') window.open(kvserverurl, '_blank')
} }
//
const goToProgressiveStep = () => {
currentStep.value = 5
}
//
const progressValue = computed(() => {
const done = Object.values(stepStates.value).filter(Boolean).length
return (done / 4) * 100
})
const statusColor = computed(() => {
return progressiveStatus.value === 'success'
? 'success'
: progressiveStatus.value === 'error'
? 'error'
: 'primary'
})
const statusIcon = computed(() => {
return progressiveStatus.value === 'success'
? 'mdi-check-circle'
: progressiveStatus.value === 'error'
? 'mdi-alert-circle'
: progressiveStatus.value === 'registering'
? 'mdi-progress-clock'
: 'mdi-rocket-launch'
})
const statusTitle = computed(() => {
return progressiveStatus.value === 'success'
? '完成!设备已创建'
: progressiveStatus.value === 'error'
? '创建失败'
: progressiveStatus.value === 'registering'
? '正在执行…'
: '准备开始'
})
const addLog = (message) => {
const now = new Date()
const hh = String(now.getHours()).padStart(2, '0')
const mm = String(now.getMinutes()).padStart(2, '0')
const ss = String(now.getSeconds()).padStart(2, '0')
logs.value.push({ time: `${hh}:${mm}:${ss}`, message })
}
const generateDeviceName = () => {
return `Classworks`
}
//
const startProgressiveRegister = async () => {
if (progressiveStatus.value === 'registering') return
progressiveStatus.value = 'registering'
progressiveError.value = ''
logs.value = []
stepStates.value = { 1: false, 2: false, 3: false, 4: false }
try {
addLog('正在生成设备信息…')
const uuid = uuidv4()
const deviceName = generateDeviceName()
const serverUrl = getSetting('server.domain')
stepStates.value[1] = true
addLog('向服务器注册设备…')
const response = await axios.post(`${serverUrl}/devices`, { uuid, deviceName })
void response
stepStates.value[2] = true
//
deviceInfo.value = { uuid, deviceName, createdAt: new Date().toISOString(), registered: true }
localStorage.setItem('Classworks_progressive_device', JSON.stringify(deviceInfo.value))
addLog('获取访问令牌…')
try {
const tokenResp = await axios.post(`${serverUrl}/apps/auth/token`, {
namespace: uuid,
password: '',
appId: 'd158067f53627d2b98babe8bffd2fd7d',
})
if (tokenResp.data && tokenResp.data.token) {
// token 使
tokenData.value = tokenResp.data
setSetting('server.kvToken', tokenResp.data.token)
// device uuid
if (tokenResp.data.device?.uuid) {
setSetting('device.uuid', tokenResp.data.device.uuid)
}
addLog('已获取 Token 并写入配置')
} else {
addLog('未返回 Token您可以稍后在授权页完成配置')
}
} catch (tokenErr) {
console.warn('自动获取 Token 失败:', tokenErr)
addLog('自动获取 Token 失败,可在授权页手动完成')
}
stepStates.value[3] = true
addLog('完成!您可以应用令牌或前往授权页面继续配置')
stepStates.value[4] = true
progressiveStatus.value = 'success'
} catch (error) {
console.error('设备注册失败:', error)
progressiveError.value = error.response?.data?.message || error.message || '网络连接失败'
addLog('失败:' + progressiveError.value)
progressiveStatus.value = 'error'
}
}
const retryProgressiveRegister = () => {
progressiveStatus.value = 'idle'
progressiveError.value = ''
logs.value = []
stepStates.value = { 1: false, 2: false, 3: false, 4: false }
}
const openAuthPage = () => {
const info = deviceInfo.value
if (!info?.uuid) return
const authDomain = getSetting('server.authDomain')
const url = `${authDomain}/?uuid=${encodeURIComponent(info.uuid)}&tolinktoaccount=true`
window.open(url, '_blank')
}
// success
const applyTokenAndClose = () => {
if (tokenData.value) {
// success token DeviceAuthDialog
emit('success', tokenData.value)
}
//
emit('close')
}
</script> </script>
<style scoped> <style scoped>
@ -668,4 +845,50 @@ const openKVSite = () => {
margin: 20px 0; margin: 20px 0;
} }
} }
/* 渐进式注册卡片样式 */
.progressive-register-card {
transition: all 0.3s ease;
border: 2px solid transparent !important;
}
.progressive-register-card:hover {
box-shadow: 0 8px 24px rgba(0,0,0,0.12) !important;
}
.progressive-register-card .card-icon-wrapper {
flex-shrink: 0;
}
.progressive-register-card .card-actions {
flex-shrink: 0;
}
.progressive-register-card code {
background: rgba(0,0,0,0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
/* 渐进式注册页面样式 */
.log-box {
height: 140px;
overflow: auto;
background: rgba(0, 0, 0, 0.04);
border-radius: 8px;
padding: 8px 12px;
}
.log-line + .log-line {
margin-top: 4px;
}
.device-code {
background: rgba(0, 0, 0, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
}
</style> </style>

View File

@ -0,0 +1,304 @@
<template>
<v-card>
<v-card-title class="d-flex align-center">
<v-icon
icon="mdi-account-plus"
class="mr-2"
/>
渐进式注册
</v-card-title>
<v-card-text>
<div v-if="!isRegistered && !isRegistering">
<p class="text-body-1 mb-4">
快速创建设备并开始使用 Classworks 云端功能
</p>
<v-alert
type="info"
variant="tonal"
class="mb-4"
>
<template #prepend>
<v-icon icon="mdi-information" />
</template>
系统将自动为您创建设备并获取访问令牌无需手动配置
</v-alert>
</div>
<!-- 注册进行中 -->
<div v-else-if="isRegistering">
<div class="text-center py-4">
<v-progress-circular
indeterminate
size="48"
color="primary"
class="mb-4"
/>
<p class="text-h6 mb-2">
正在注册设备...
</p>
<p class="text-body-2 text-medium-emphasis">
{{ registrationStep }}
</p>
</div>
</div>
<!-- 注册成功 -->
<div v-else-if="isRegistered && deviceInfo">
<v-alert
type="success"
variant="tonal"
class="mb-4"
>
<template #prepend>
<v-icon icon="mdi-check-circle" />
</template>
设备注册成功已自动获取访问令牌
</v-alert>
<v-list>
<v-list-item>
<template #prepend>
<v-icon icon="mdi-identifier" />
</template>
<v-list-item-title>设备名称</v-list-item-title>
<v-list-item-subtitle>{{ deviceInfo.deviceName }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon icon="mdi-key" />
</template>
<v-list-item-title>设备 UUID</v-list-item-title>
<v-list-item-subtitle class="font-mono text-caption">
{{ deviceInfo.uuid }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
<v-alert
type="info"
variant="tonal"
class="mt-4"
>
<template #prepend>
<v-icon icon="mdi-information" />
</template>
您可以点击下方按钮访问云端控制台来设置密码和管理高级功能
</v-alert>
</div>
<!-- 错误状态 -->
<div v-else-if="errorMessage">
<v-alert
type="error"
variant="tonal"
class="mb-4"
>
<template #prepend>
<v-icon icon="mdi-alert-circle" />
</template>
{{ errorMessage }}
</v-alert>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<!-- 注册按钮 -->
<v-btn
v-if="!isRegistered && !isRegistering"
color="primary"
prepend-icon="mdi-plus"
:loading="isRegistering"
@click="registerDevice"
>
注册设备
</v-btn>
<!-- 访问控制台按钮 -->
<v-btn
v-if="isRegistered && deviceInfo"
color="success"
prepend-icon="mdi-open-in-new"
@click="openConsole"
>
访问控制台
</v-btn>
<!-- 重试按钮 -->
<v-btn
v-if="errorMessage"
color="primary"
prepend-icon="mdi-refresh"
@click="resetAndRetry"
>
重试
</v-btn>
<v-btn
variant="text"
@click="$emit('close')"
>
关闭
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup>
import { ref } from 'vue'
import { getSetting, setSetting } from '@/utils/settings'
import axios from '@/axios/axios'
//
const emit = defineEmits(['close', 'success'])
//
const PROGRESSIVE_DEVICE_KEY = 'Classworks_progressive_device'
//
const isRegistering = ref(false)
const isRegistered = ref(false)
const deviceInfo = ref(null)
const errorMessage = ref('')
const registrationStep = ref('')
// UUID
const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
//
const generateDeviceName = () => {
const adjectives = ['快速', '智能', '高效', '便捷', '实用', '简洁', '现代', '专业']
const nouns = ['设备', '终端', '工作站', '助手', '伴侣']
const numbers = Math.floor(Math.random() * 999) + 1
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]
const noun = nouns[Math.floor(Math.random() * nouns.length)]
return `${adjective}${noun}${numbers}`
}
//
const saveDeviceInfo = (info) => {
try {
localStorage.setItem(PROGRESSIVE_DEVICE_KEY, JSON.stringify(info))
deviceInfo.value = info
} catch (error) {
console.error('保存设备信息失败:', error)
}
}
//
const registerDevice = async () => {
if (isRegistering.value) return
isRegistering.value = true
errorMessage.value = ''
registrationStep.value = '正在生成设备信息...'
try {
const uuid = generateUUID()
const deviceName = generateDeviceName()
const serverUrl = getSetting('server.domain')
registrationStep.value = '正在注册设备到服务器...'
console.log('开始注册设备:', { uuid, deviceName, serverUrl })
//
const response = await axios.post(`${serverUrl}/devices`, {
uuid,
deviceName
})
console.log('设备注册响应:', response.data)
//
const newDeviceInfo = {
uuid,
deviceName,
createdAt: new Date().toISOString(),
registered: true
}
saveDeviceInfo(newDeviceInfo)
registrationStep.value = '正在获取访问令牌...'
// token使 uuid namespace
await autoLogin(uuid)
isRegistered.value = true
} catch (error) {
console.error('设备注册失败:', error)
errorMessage.value = error.response?.data?.message || error.message || '网络连接失败'
} finally {
isRegistering.value = false
registrationStep.value = ''
}
}
// token
const autoLogin = async (uuid) => {
try {
const serverUrl = getSetting('server.domain')
// 使 token
const response = await axios.post(`${serverUrl}/apps/auth/token`, {
namespace: uuid,
password: '' //
})
if (response.data && response.data.token) {
// token
setSetting('server.kvToken', response.data.token)
console.log('自动登录成功token 已设置')
//
emit('success', response.data)
}
} catch (error) {
console.error('自动登录失败:', error)
throw new Error('获取访问令牌失败: ' + (error.response?.data?.message || error.message))
}
}
//
const openConsole = () => {
if (!deviceInfo.value) return
const authDomain = getSetting('server.authDomain')
const url = `${authDomain}/?uuid=${encodeURIComponent(deviceInfo.value.uuid)}`
console.log('打开控制台:', url)
window.open(url, '_blank')
}
//
const resetAndRetry = () => {
errorMessage.value = ''
isRegistered.value = false
deviceInfo.value = null
//
try {
localStorage.removeItem(PROGRESSIVE_DEVICE_KEY)
} catch (error) {
console.error('清除本地存储失败:', error)
}
}
</script>
<style scoped>
.font-mono {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
</style>

View File

@ -4,54 +4,115 @@
<v-progress-linear v-if="loading" indeterminate color="primary" /> <v-progress-linear v-if="loading" indeterminate color="primary" />
</template> </template>
<v-card-title> <v-card-title>
<v-icon class="me-2"> mdi-cloud-check </v-icon> <v-icon
class="me-2"
>
mdi-cloud-check
</v-icon>
设备信息 设备信息
</v-card-title> </v-card-title>
<v-card-text v-if="hasNamespaceInfo"> <v-card-text v-if="hasNamespaceInfo">
<!-- 用户信息与头像 --> <!-- 未绑定账号时的提示卡片 -->
<div v-if="namespaceInfo.account" class="d-flex align-center mb-4"> <div
v-if="namespaceInfo.hasAccount === false"
class="mb-4"
>
<v-alert
type="warning"
variant="tonal"
border
>
<v-alert-title>设备未绑定账号</v-alert-title>
<div>当前设备尚未绑定账号,部分功能可能受限请前往绑定账号以获得完整体验</div>
<v-btn
class="mt-3"
variant="outlined"
:href="getBindAccountUrl()"
append-icon="mdi-open-in-new"
target="_blank"
>
前往绑定账号
</v-btn>
</v-alert>
</div>
<!-- 已绑定账号时显示账号信息 -->
<div
v-if="namespaceInfo.hasAccount && namespaceInfo.account"
class="d-flex align-center mb-4"
>
<v-card <v-card
border hover border
hover
class="w-100" class="w-100"
variant="tonal" variant="tonal"
:prepend-avatar="namespaceInfo.account.avatarUrl" :prepend-avatar="namespaceInfo.account.avatarUrl"
:title="namespaceInfo.account.name || '未命名用户'" :title="namespaceInfo.account.name || '未命名用户'"
:subtitle=" :subtitle="'此设备由贵校管理 管理员账号 ID: ' + namespaceInfo.account.id"
'此设备由贵校管理 管理员账号 ID: ' + namespaceInfo.account.id
"
>
<v-card-text
>此设备由贵校或贵单位管理该管理员系此空间所有者如有疑问请咨询他对于恶意绑定滥用行为请反馈</v-card-text
> >
<v-card-text>
此设备由贵校或贵单位管理该管理员系此空间所有者如有疑问请咨询他对于恶意绑定滥用行为请反馈
</v-card-text>
</v-card> </v-card>
</div> </div>
<!-- 设备信息卡片 --> <!-- 设备信息卡片 -->
<v-card v-if="namespaceInfo.device" variant="tonal" class="mb-4" border hover> <v-card
<v-card-title class="pb-1"> 设备信息 </v-card-title> v-if="namespaceInfo.device"
variant="tonal"
class="mb-4"
border
hover
>
<v-card-title class="pb-1">
设备信息
</v-card-title>
<v-card-text> <v-card-text>
<div class="d-flex flex-column gap-1"> <div class="d-flex flex-column gap-1">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon size="small" class="me-2"> mdi-tag </v-icon> <v-icon
size="small"
class="me-2"
>
mdi-tag
</v-icon>
<span class="font-weight-medium me-2">设备名称:</span> <span class="font-weight-medium me-2">设备名称:</span>
<span>{{ namespaceInfo.device.name || "未命名设备" }}</span> <span>{{ namespaceInfo.device.name || '未命名设备' }}</span>
</div> </div>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon size="small" class="me-2"> mdi-identifier </v-icon> <v-icon
size="small"
class="me-2"
>
mdi-identifier
</v-icon>
<span class="font-weight-medium me-2">设备 ID:</span> <span class="font-weight-medium me-2">设备 ID:</span>
<span>{{ namespaceInfo.device.id }}</span> <span>{{ namespaceInfo.device.id }}</span>
</div> </div>
<div class="d-flex align-center"> <!-- 仅未绑定账号时显示 UUID -->
<v-icon size="small" class="me-2"> mdi-uuid </v-icon> <div
v-if="namespaceInfo.hasAccount === false && namespaceInfo.device.uuid"
class="d-flex align-center"
>
<v-icon
size="small"
class="me-2"
>
mdi-uuid
</v-icon>
<span class="font-weight-medium me-2">UUID:</span> <span class="font-weight-medium me-2">UUID:</span>
<span class="text-truncate">{{ <span class="text-truncate">{{ namespaceInfo.device.uuid }}</span>
namespaceInfo.device.uuid || "未知"
}}</span>
</div> </div>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon size="small" class="me-2"> mdi-calendar </v-icon> <v-icon
size="small"
class="me-2"
>
mdi-calendar
</v-icon>
<span class="font-weight-medium me-2">创建时间:</span> <span class="font-weight-medium me-2">创建时间:</span>
<span>{{ formatDate(namespaceInfo.device.createdAt) }}</span> <span>{{ formatDate(namespaceInfo.device.createdAt) }}</span>
</div> </div>
@ -59,33 +120,54 @@
v-if="namespaceInfo.device.updatedAt" v-if="namespaceInfo.device.updatedAt"
class="d-flex align-center" class="d-flex align-center"
> >
<v-icon size="small" class="me-2"> mdi-calendar-clock </v-icon> <v-icon
size="small"
class="me-2"
>
mdi-calendar-clock
</v-icon>
<span class="font-weight-medium me-2">更新时间:</span> <span class="font-weight-medium me-2">更新时间:</span>
<span>{{ formatDate(namespaceInfo.device.updatedAt) }}</span> <span>{{ formatDate(namespaceInfo.device.updatedAt) }}</span>
</div> </div>
</div> </div>
</v-card-text> </v-card </v-card-text>
><v-card title="Classworks KV" subtitle="文档形键值数据库" border hover </v-card>
><v-card-text
>Classworks KV <v-card
是厚浪云推出的文档形键值数据库其是一个开放的云应用平台为各种应用提供存储服务此设备正在使用其服务如果您希望管理设备信息请前往 title="Classworks KV"
Classworks KV subtitle="文档形键值数据库"
的网站如果您在服务推出前就在使用 Classworks您的数据已被自动迁移 border
<br/><br/>Classworks KV 的全域管理员是 <a href="https://wuyuan.dev" target="_blank">孙悟元</a></v-card-text hover
><v-card-actions >
><v-btn <v-card-text>
Classworks KV 是厚浪云推出的文档形键值数据库其是一个开放的云应用平台为各种应用提供存储服务此设备正在使用其服务如果您希望管理设备信息请前往 Classworks KV 的网站如果您在服务推出前就在使用 Classworks您的数据已被自动迁移
<br><br>
Classworks KV 的全域管理员是
<a
href="https://wuyuan.dev"
target="_blank"
>
孙悟元
</a>
</v-card-text>
<v-card-actions>
<v-btn
:href="defaultAuthServer" :href="defaultAuthServer"
class="text-none" class="text-none"
append-icon="mdi-open-in-new" append-icon="mdi-open-in-new"
target="_blank" target="_blank"
>前往 Classworks KV</v-btn
></v-card-actions
></v-card
> >
前往 Classworks KV
</v-btn>
</v-card-actions>
</v-card>
</v-card-text> </v-card-text>
<v-card-text v-else> <v-card-text v-else>
<v-alert type="info" variant="tonal"> <v-alert
type="info"
variant="tonal"
>
<v-alert-title>未获取到设备信息</v-alert-title> <v-alert-title>未获取到设备信息</v-alert-title>
<p>您尚未完成云端存储授权或连接失败请点击下方按钮进行初始化</p> <p>您尚未完成云端存储授权或连接失败请点击下方按钮进行初始化</p>
</v-alert> </v-alert>
@ -102,17 +184,28 @@
刷新设备信息 刷新设备信息
</v-btn> </v-btn>
<v-btn color="error" variant="outlined" @click="showReinitDialog = true"> <v-btn
color="error"
variant="outlined"
@click="showReinitDialog = true"
>
重新初始化云端存储 重新初始化云端存储
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
<!-- 重新初始化确认对话框 --> <!-- 重新初始化确认对话框 -->
<v-dialog v-model="showReinitDialog" max-width="500"> <v-dialog
v-model="showReinitDialog"
max-width="500"
>
<v-card> <v-card>
<v-card-title>确认重新初始化</v-card-title> <v-card-title>确认重新初始化</v-card-title>
<v-card-text> <v-card-text>
<v-alert type="warning" variant="tonal" class="mb-3"> <v-alert
type="warning"
variant="tonal"
class="mb-3"
>
<v-alert-title>警告</v-alert-title> <v-alert-title>警告</v-alert-title>
此操作将清除当前的云端存储配置包括 Token您需要重新进行授权 此操作将清除当前的云端存储配置包括 Token您需要重新进行授权
</v-alert> </v-alert>
@ -120,8 +213,18 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn variant="text" @click="showReinitDialog = false">取消</v-btn> <v-btn
<v-btn color="error" @click="confirmReinitialize">确认</v-btn> variant="text"
@click="showReinitDialog = false"
>
取消
</v-btn>
<v-btn
color="error"
@click="confirmReinitialize"
>
确认
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -190,6 +293,13 @@ export default {
async reloadInfo() { async reloadInfo() {
await this.fetchNamespaceInfo(); await this.fetchNamespaceInfo();
}, },
getBindAccountUrl() {
const uuid = this.namespaceInfo?.device?.uuid;
if (uuid) {
return `${this.defaultAuthServer}?uuid=${encodeURIComponent(uuid)}&tolinktoaccount=true`;
}
return this.defaultAuthServer;
},
confirmReinitialize() { confirmReinitialize() {
// token shouldShowInit // token shouldShowInit
setSetting('server.kvToken', ''); setSetting('server.kvToken', '');