1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-07 21:13:11 +00:00
Classworks/src/components/auth/FirstTimeGuide.vue
copilot-swe-agent[bot] f7f703466f feat: Add configurable prompt text for homework edit dialog
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-11-19 13:07:03 +00:00

895 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-card class="guide-card">
<!-- 进度指示器 -->
<v-progress-linear
:model-value="(currentStep / totalSteps) * 100"
color="primary"
height="6"
/>
<v-card-text class="pa-8">
<!-- 步骤 1: 欢迎 -->
<div
v-show="currentStep === 1"
class="step-content"
>
<div class="text-center mb-6">
<v-icon
class="mb-4"
color="primary"
size="80"
>
mdi-hand-wave
</v-icon>
<h2 class="text-h4 mb-3">
欢迎使用 Classworks
</h2>
<p class="text-body-1 text-medium-emphasis">
适用于班级大屏的作业板小工具
</p>
</div>
</div>
<!-- 步骤 2: Classworks KV 的关系图 -->
<div
v-show="currentStep === 2"
class="step-content"
>
<h3 class="text-h5 mb-6 text-center">
Classworks Classworks KV 的关系
</h3>
<v-card
class="pa-6 mb-6"
color="primary"
variant="tonal"
>
<div class="relationship-diagram">
<!-- Classworks 应用 -->
<div class="diagram-item">
<v-card
class="pa-4"
color="blue-darken-1"
elevation="8"
>
<div class="text-center">
<v-icon
color="white"
size="60"
>
mdi-laptop
</v-icon>
<h4 class="text-h6 text-white mt-2">
Classworks
</h4>
<p class="text-caption text-white mt-1">
作业板应用
</p>
</div>
</v-card>
<div class="diagram-description mt-3">
<v-chip
class="mb-2"
color="blue"
size="small"
variant="flat"
>
前端应用
</v-chip>
<div class="text-body-2">
显示作业内容<br>
管理班级信息<br>
提供用户界面
</div>
</div>
</div>
<!-- 连接线 -->
<div class="diagram-connector">
<v-icon
color="primary"
size="40"
>
mdi-swap-horizontal
</v-icon>
<div class="text-caption font-weight-bold mt-2">
数据同步
</div>
</div>
<!-- Classworks KV -->
<div class="diagram-item">
<v-card
class="pa-4"
color="green-darken-1"
elevation="8"
>
<div class="text-center">
<v-icon
color="white"
size="60"
>
mdi-cloud-sync
</v-icon>
<h4 class="text-h6 text-white mt-2">
Classworks KV
</h4>
<p class="text-caption text-white mt-1">
云端数据库
</p>
</div>
</v-card>
<div class="diagram-description mt-3">
<v-chip
class="mb-2"
color="green"
size="small"
variant="flat"
>
后端服务
</v-chip>
<div class="text-body-2">
存储作业数据<br>
多设备同步<br>
权限管理
</div>
</div>
</div>
</div>
</v-card>
</div>
<!-- 步骤 3: 询问使用场景 -->
<div
v-show="currentStep === 3"
class="step-content"
>
<h3 class="text-h5 mb-6 text-center">
你需要在多个设备上查看作业吗
</h3>
<v-card
class="mb-6 pa-4"
color="info"
variant="tonal"
>
<div class="text-body-2">
比如在家里电脑手机上查看或者多个教室设备共享数据
</div>
</v-card>
<div class="button-group">
<v-btn
block
class="mb-4 py-6"
color="primary"
size="x-large"
variant="elevated"
@click="selectStorageType('cloud')"
>
<div class="d-flex flex-column align-center py-2">
<v-icon
class="mb-2"
size="40"
>
mdi-cloud-check
</v-icon>
<span class="text-h6">需要使用云同步</span>
<span class="text-caption mt-1">多设备访问</span>
</div>
</v-btn>
<v-btn
block
class="py-6"
size="x-large"
variant="outlined"
@click="selectStorageType('local')"
>
<div class="d-flex flex-column align-center py-2">
<v-icon
class="mb-2"
size="40"
>
mdi-laptop
</v-icon>
<span class="text-h6">不需要只用这台设备</span>
<span class="text-caption mt-1">本地存储</span>
</div>
</v-btn>
</div>
</div>
<!-- 步骤 4a: 本地存储确认 -->
<div
v-show="currentStep === 4 && storageType === 'local'"
class="step-content"
>
<div class="text-center mb-6">
<v-icon
class="mb-4"
color="success"
size="80"
>
mdi-check-circle
</v-icon>
<h3 class="text-h5 mb-4">
您可以使用本地模式
</h3>
<v-card
class="pa-4 text-left"
variant="tonal"
>
<div class="text-body-1 mb-2">
此数据将存储在您的浏览器中如果您的浏览器不支持IndexedDB可能会出现问题如果您经常清除浏览器数据请谨慎使用本地模式
</div>
<div class="text-body-1 mb-2">
在刚才地方点击使用本地模式的按钮使用
</div>
</v-card>
</div>
</div>
<!-- 步骤 4b: 云存储说明 -->
<div
v-show="currentStep === 4 && storageType === 'cloud'"
class="step-content"
>
<div class="text-center mb-6">
<v-icon
class="mb-4"
color="primary"
size="80"
>
mdi-cloud-cog
</v-icon>
<h3 class="text-h5 mb-4">
需要先设置云端账号
</h3>
</div>
<v-card
class="pa-6 mb-6"
variant="tonal"
>
<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"
prepend-icon="mdi-flash"
size="large"
variant="elevated"
@click="goToProgressiveStep"
>
自动注册
</v-btn>
</div>
</div>
</v-card>
<div class="mb-6">
也可以手动前往 Classworks KV 控制台获取认证信息
</div>
<v-card
:color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' "
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
class="pa-6 mb-6"
@click="openKVSite"
>
<v-icon
class="mb-3"
size="48"
>
mdi-open-in-new
</v-icon>
<h4 class="text-h6 font-weight-bold">
请访问 {{ kvserverurl=='https://kv.houlang.cloud'? 'Classworks KV' : '自定义的 Classworks KV 实例 ' }} 控制台
</h4>
<div class="text-h5 mb-6">
{{ kvserverurl }}
</div>
<h6 class="text-subtitle-2">
{{ kvserverurl=='https://kv.houlang.cloud'? '此实例由 Classworks KV 官方提供' : '此链接由您的实例、预配代码或管理员管理,当前可能不是 Classworks KV 官方的实例地址。' }}
</h6>
</v-card>
<!-- 常见问题 -->
<v-expansion-panels
class="mt-6"
variant="accordion"
>
<v-expansion-panel>
<v-expansion-panel-title>
<div class="d-flex align-center">
<v-icon
class="mr-3"
color="warning"
>
mdi-help-circle
</v-icon>
<span class="text-subtitle-1 font-weight-medium">我以前已经使用过 Classworks KV</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card
class="pa-4"
color="success"
variant="tonal"
>
<div class="text-body-2 mb-2">
如果您之前已经使用过 Classworks KV可以直接使用您的 <strong>UUID命名空间</strong>
<strong>设置的密码</strong> 进行认证
</div>
<div class="text-body-2">
返回上一页点击"已注册"按钮输入您的认证信息即可登录
</div>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title>
<div class="d-flex align-center">
<v-icon
class="mr-3"
color="info"
>
mdi-help-circle
</v-icon>
<span class="text-subtitle-1 font-weight-medium">我如何配置不同类型的设备</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card
class="pa-4"
color="info"
variant="tonal"
>
<div class="text-body-2 mb-2">
不同的密码对应不同的设备类型这将由 <strong>管理员管理</strong>
</div>
<div class="text-body-2 mb-2">
例如
</div>
<ul class="text-body-2 ml-4">
<li class="mb-1">
班级大屏使用一个密码
</li>
<li class="mb-1">
教师设备使用另一个密码
</li>
<li>学生设备使用不同的密码</li>
</ul>
<div class="text-body-2 mt-3">
请联系您的管理员获取对应设备类型的密码
</div>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
<!-- 步骤 5: 渐进式注册完整流程 -->
<div
v-show="currentStep === 5"
class="step-content"
>
<div class="text-center mb-6">
<v-avatar
class="mb-4"
color="primary"
size="80"
variant="tonal"
>
<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"
class="mb-6"
color="primary"
height="8"
rounded
/>
<v-row>
<v-col
cols="12"
>
<v-card
:color="statusColor"
variant="tonal"
>
<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"
prepend-icon="mdi-play"
size="large"
@click="startProgressiveRegister"
>
开始创建
</v-btn>
<v-btn
v-if="progressiveStatus === 'error'"
color="error"
prepend-icon="mdi-refresh"
variant="outlined"
@click="retryProgressiveRegister"
>
重试
</v-btn>
<v-btn
v-if="progressiveStatus === 'registering'"
:loading="true"
color="primary"
prepend-icon="mdi-progress-clock"
variant="tonal"
>
正在执行
</v-btn>
<v-btn
v-if="progressiveStatus === 'success'"
color="success"
prepend-icon="mdi-check-circle"
size="large"
variant="elevated"
@click="applyTokenAndClose"
>
应用令牌并关闭
</v-btn>
<v-btn
v-if="progressiveStatus === 'success'"
color="primary"
prepend-icon="mdi-open-in-new"
size="large"
variant="outlined"
@click="openAuthPage"
>
前往绑定账户
</v-btn>
</div>
</div>
</v-card-text>
<!-- 底部操作按钮 -->
<v-card-actions class="pa-6 pt-0">
<v-btn
v-if="currentStep > 1"
size="large"
variant="text"
@click="prevStep"
>
<v-icon start>
mdi-chevron-left
</v-icon>
上一步
</v-btn>
<v-spacer />
<v-btn
v-if="currentStep < totalSteps && currentStep !== 4"
:disabled="currentStep === 3 && !storageType"
color="primary"
size="large"
variant="elevated"
@click="nextStep"
>
下一步
<v-icon end>
mdi-chevron-right
</v-icon>
</v-btn>
<v-btn
v-if="currentStep === totalSteps || currentStep === 4"
color="primary"
size="large"
variant="elevated"
@click="finish"
>
关闭
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup>
import {ref, computed} from 'vue'
import {getSetting, setSetting} from '@/utils/settings'
import axios from '@/axios/axios'
import {v4 as uuidv4} from 'uuid'
const emit = defineEmits(['close', 'success'])
const kvserverurl = getSetting('server.authDomain')
const currentStep = ref(1)
const totalSteps = 5
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 = () => {
if (currentStep.value < totalSteps) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 1) {
currentStep.value--
}
}
const selectStorageType = (type) => {
storageType.value = type
nextStep()
}
const finish = () => {
emit('close')
}
const openKVSite = () => {
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>
<style scoped>
.guide-card {
max-width: 100%;
min-height: 500px;
}
.step-content {
min-height: 400px;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.button-group {
max-width: 600px;
margin: 0 auto;
}
.step-item {
cursor: default;
}
/* 触屏优化 */
.v-btn {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* 大按钮区域便于点击 */
.v-btn.v-btn--size-x-large {
min-height: 120px;
}
/* 关系图样式 */
.relationship-diagram {
display: flex;
justify-content: space-around;
align-items: flex-start;
gap: 20px;
flex-wrap: wrap;
}
.diagram-item {
flex: 1;
min-width: 200px;
max-width: 300px;
}
.diagram-connector {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 100px;
}
.diagram-description {
text-align: center;
}
@media (max-width: 768px) {
.relationship-diagram {
flex-direction: column;
align-items: center;
}
.diagram-connector {
transform: rotate(90deg);
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>