query-database/src/pages/OnboardingPage.vue
2026-03-25 15:46:20 +08:00

239 lines
9.0 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.

<script setup lang="ts">
import { getToken, refreshMe, useAuthState } from '@/composables/useAuth'
import type { HttpError } from '@/lib/http'
import { http } from '@/lib/http'
import { computed, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const auth = useAuthState()
const step = ref(1)
const name = ref('')
const moduleKey = ref<'shop' | 'hr'>('shop')
const experienceLevel = ref<'beginner' | 'normal' | 'advanced'>('beginner')
const dbMode = ref<'mock' | 'import'>('mock')
const mockDbKey = ref<'shop' | 'hr'>('shop')
const loading = ref(false)
const error = ref<string | null>(null)
const canNext = computed(() => {
if (step.value === 1) return name.value.trim().length >= 1
return true
})
watch(
() => moduleKey.value,
(v) => {
if (dbMode.value === 'mock') mockDbKey.value = v
},
{ immediate: true },
)
watch(
() => dbMode.value,
(v) => {
if (v === 'mock') mockDbKey.value = moduleKey.value
},
)
async function next() {
if (!canNext.value) return
step.value = Math.min(4, step.value + 1)
}
function prev() {
step.value = Math.max(1, step.value - 1)
}
async function complete() {
error.value = null
loading.value = true
try {
const token = getToken()
await http('/api/onboarding/complete', {
method: 'POST',
token,
body: {
name: name.value.trim(),
moduleKey: moduleKey.value,
experienceLevel: experienceLevel.value,
dbMode: dbMode.value,
mockDbKey: dbMode.value === 'mock' ? mockDbKey.value : null,
},
})
await refreshMe()
if (dbMode.value === 'import') router.replace({ name: 'databases' })
else router.replace({ name: 'home' })
} catch (e) {
const he = e as HttpError
error.value = he.message || '提交失败'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-full">
<div class="mx-auto max-w-3xl px-4 py-10">
<div class="mb-6">
<div class="text-lg font-semibold">首次引导</div>
<div class="mt-1 text-sm text-zinc-400"> 1 分钟完成设置就可以开始练习</div>
</div>
<div class="rounded-2xl border border-zinc-800 bg-zinc-950 p-6">
<div class="mb-5 flex items-center justify-between">
<div class="text-sm text-zinc-400"> {{ step }} / 4 </div>
<div class="text-sm text-zinc-300">{{ auth.me?.email }}</div>
</div>
<div v-if="step === 1" class="space-y-3">
<div class="text-sm font-medium">先告诉我你的名字</div>
<input
v-model="name"
class="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm outline-none focus:border-blue-600"
placeholder="例如:小明"
/>
</div>
<div v-else-if="step === 2" class="space-y-3">
<div class="text-sm font-medium">你感兴趣的模块</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<button
class="rounded-xl border px-4 py-3 text-left"
:class="moduleKey === 'shop' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="moduleKey = 'shop'"
>
<div class="text-sm font-medium">电商</div>
<div class="mt-1 text-xs text-zinc-400">商品/订单/用户</div>
</button>
<button
class="rounded-xl border px-4 py-3 text-left"
:class="moduleKey === 'hr' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="moduleKey = 'hr'"
>
<div class="text-sm font-medium">人事</div>
<div class="mt-1 text-xs text-zinc-400">员工/部门/薪资</div>
</button>
</div>
</div>
<div v-else-if="step === 3" class="space-y-3">
<div class="text-sm font-medium">你的经验水平</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
<button
class="rounded-xl border px-4 py-3 text-left"
:class="experienceLevel === 'beginner' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="experienceLevel = 'beginner'"
>
<div class="text-sm font-medium">新手</div>
<div class="mt-1 text-xs text-zinc-400">从 SELECT 开始</div>
</button>
<button
class="rounded-xl border px-4 py-3 text-left"
:class="experienceLevel === 'normal' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="experienceLevel = 'normal'"
>
<div class="text-sm font-medium">一般</div>
<div class="mt-1 text-xs text-zinc-400">JOIN/聚合</div>
</button>
<button
class="rounded-xl border px-4 py-3 text-left"
:class="experienceLevel === 'advanced' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="experienceLevel = 'advanced'"
>
<div class="text-sm font-medium">进阶</div>
<div class="mt-1 text-xs text-zinc-400">窗口/CTE</div>
</button>
</div>
</div>
<div v-else class="space-y-3">
<div class="text-sm font-medium">选择数据库来源</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<button
class="rounded-xl border px-4 py-3 text-left"
:class="dbMode === 'mock' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="dbMode = 'mock'"
>
<div class="text-sm font-medium">使用模拟数据库</div>
<div class="mt-1 text-xs text-zinc-400">开箱即用,适合练习</div>
</button>
<button
class="rounded-xl border px-4 py-3 text-left"
:class="dbMode === 'import' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="dbMode = 'import'"
>
<div class="text-sm font-medium">导入我的数据库</div>
<div class="mt-1 text-xs text-zinc-400">上传初始化 SQL</div>
</button>
</div>
<div v-if="dbMode === 'mock'" class="rounded-xl border border-zinc-800 bg-zinc-950 p-4">
<div class="text-xs text-zinc-400">选择一个模拟库</div>
<div class="mt-1 text-xs text-zinc-500">推荐:与模块一致的模拟库(更容易通过判题)</div>
<div class="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
<button
class="rounded-lg border px-3 py-2 text-left"
:class="mockDbKey === 'shop' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="mockDbKey = 'shop'"
>
<div class="text-sm font-medium">电商库</div>
<div class="mt-1 text-xs text-zinc-400">商品/订单</div>
</button>
<button
class="rounded-lg border px-3 py-2 text-left"
:class="mockDbKey === 'hr' ? 'border-blue-600 bg-blue-950/30' : 'border-zinc-800 hover:bg-zinc-900'"
@click="mockDbKey = 'hr'"
>
<div class="text-sm font-medium">人事库</div>
<div class="mt-1 text-xs text-zinc-400">员工/部门</div>
</button>
</div>
</div>
<div v-else class="rounded-xl border border-zinc-800 bg-zinc-950 p-4 text-sm text-zinc-300">
你可以先导入自己的库做自由练习;若要做题并获得判题结果,建议之后切换到模块对应的模拟库。
</div>
</div>
<div v-if="error" class="mt-5 rounded-lg border border-red-900 bg-red-950 px-3 py-2 text-sm text-red-200">
{{ error }}
</div>
<div class="mt-6 flex items-center justify-between">
<button
class="rounded-lg px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-900 disabled:opacity-50"
:disabled="step === 1 || loading"
@click="prev"
>
上一步
</button>
<div class="flex items-center gap-2">
<button
v-if="step < 4"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-60"
:disabled="!canNext || loading"
@click="next"
>
下一步
</button>
<button
v-else
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-60"
:disabled="loading"
@click="complete"
>
{{ loading ? '提交中…' : '完成并进入系统' }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>