239 lines
9.0 KiB
Vue
239 lines
9.0 KiB
Vue
<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>
|
||
|