feat(S4): new user onboarding wizard with 3-step guided flow

- Backend: check-login API returns is_new_user flag (no prompt history)
- Vue: OnboardingWizard component (scene select → tips → ready)
- Vue: HomeView conditionally shows wizard for new users
- Auth store: expose isNewUser state, auto-detect on refresh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-03 09:54:32 +08:00
parent 5cd7e1eb30
commit e2c0b6b87a
5 changed files with 238 additions and 39 deletions

View File

@@ -138,11 +138,20 @@ def change_password():
def check_login():
"""检查登录状态API"""
if 'user_id' in session:
uid = session['user_id']
# 判断是否新用户(无任何生成记录)
is_new = False
try:
from src.flask_prompt_master.models.models import Prompt
is_new = Prompt.query.filter_by(user_id=uid).count() == 0
except Exception:
pass
return jsonify({
'success': True,
'logged_in': True,
'is_new_user': is_new,
'user': {
'user_id': session['user_id'],
'user_id': uid,
'nickname': session.get('nickname'),
'login_name': session.get('login_name')
}
@@ -150,7 +159,8 @@ def check_login():
else:
return jsonify({
'success': True,
'logged_in': False
'logged_in': False,
'is_new_user': True, # 未登录也算新用户, 引导注册
})
@auth_bp.route('/api/profile/stats', methods=['GET'])

View File

@@ -1,6 +1,7 @@
export interface CheckLoginResponse {
success: boolean
logged_in: boolean
is_new_user?: boolean
user?: {
user_id: number
nickname?: string | null

View File

@@ -0,0 +1,179 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const emit = defineEmits<{ (e: 'done'): void }>()
const activeStep = ref(0)
const router = useRouter()
const steps = [
{
title: '选择场景',
description: '选择你今天的任务类型',
},
{
title: '描述需求',
description: '用自然语言告诉我们你想要什么',
},
{
title: '获得成果',
description: 'AI 将为你生成专业内容',
},
]
const scenes = [
{ icon: '✨', name: '生成提示词', route: '/generate', desc: '将简单想法变为专业 Prompt' },
{ icon: '🍽️', name: '饭菜规划', route: '/meal-planning', desc: '根据口味预算定制菜单' },
{ icon: '📝', name: '周报生成', route: '/weekly-report', desc: '工作要点秒变周报' },
{ icon: '📄', name: '简历优化', route: '/resume-optimization', desc: '简历匹配岗位描述' },
{ icon: '📖', name: '古诗词解析', route: '/poetry', desc: '深度解读古典诗词' },
{ icon: '✈️', name: '旅行攻略', route: '/travel-planning', desc: '定制个性化行程' },
]
function goToScene(route: string) {
if (route === '/generate') {
activeStep.value = 1
}
router.push(route)
}
function nextStep() {
if (activeStep.value < 2) activeStep.value++
}
function prevStep() {
if (activeStep.value > 0) activeStep.value--
}
function finish() {
emit('done')
}
</script>
<template>
<div class="onboarding-wizard">
<el-steps :active="activeStep" finish-status="success" align-center>
<el-step v-for="(s, i) in steps" :key="i" :title="s.title" :description="s.description" />
</el-steps>
<!-- Step 0: Scene selection -->
<div v-if="activeStep === 0" class="step-content">
<h3>👋 欢迎使用提示词大师选择一个场景开始吧</h3>
<el-row :gutter="16">
<el-col v-for="s in scenes" :key="s.name" :xs="12" :sm="8" :md="4">
<el-card shadow="hover" class="scene-card" @click="goToScene(s.route)">
<div class="scene-icon">{{ s.icon }}</div>
<div class="scene-name">{{ s.name }}</div>
<div class="scene-desc">{{ s.desc }}</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- Step 1: Quick tips -->
<div v-if="activeStep === 1" class="step-content">
<h3>💡 输入提示</h3>
<el-alert
title="用自然语言描述你的需求即可"
type="success"
:closable="false"
show-icon
style="margin-bottom: 16px"
/>
<div class="tips">
<p><strong>好的描述示例</strong></p>
<el-tag type="info" size="large" style="display: block; margin-bottom: 8px">
"我需要写一个 Python 脚本,从数据库读取数据并生成 Excel 报表"
</el-tag>
<el-tag type="info" size="large" style="display: block; margin-bottom: 8px">
"我想写一篇面向初学者的 Git 使用教程500 字左右"
</el-tag>
<el-tag type="info" size="large" style="display: block">
"帮我优化这句产品文案,让它更吸引人:'我们的软件能帮你管理任务'"
</el-tag>
</div>
<div class="step-actions">
<el-button @click="prevStep">上一步</el-button>
<el-button type="primary" @click="finish">开始使用</el-button>
</div>
</div>
<!-- Step 2: Result hint -->
<div v-if="activeStep === 2" class="step-content">
<h3>🎉 开始你的第一次生成吧</h3>
<ul>
<li>生成结果可以 <strong>继续优化</strong>多轮对话迭代</li>
<li>满意的结果可以 <strong>加入收藏</strong></li>
<li>支持 <strong>导出 Markdown / PDF</strong></li>
<li> <strong>历史记录</strong> 中查看所有生成</li>
</ul>
<div class="step-actions">
<el-button @click="prevStep">上一步</el-button>
<el-button type="primary" @click="finish">知道了开始吧</el-button>
</div>
</div>
</div>
</template>
<style scoped>
.onboarding-wizard {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1rem;
}
.step-content {
margin-top: 2rem;
text-align: center;
}
.step-content h3 {
margin-bottom: 1.5rem;
font-size: 1.3rem;
}
.scene-card {
cursor: pointer;
text-align: center;
margin-bottom: 12px;
transition: transform 0.15s;
}
.scene-card:hover {
transform: translateY(-3px);
}
.scene-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.scene-name {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.3rem;
}
.scene-desc {
font-size: 0.8rem;
color: #909399;
}
.tips {
text-align: left;
max-width: 600px;
margin: 0 auto;
}
.step-actions {
margin-top: 2rem;
}
ul {
text-align: left;
max-width: 500px;
margin: 0 auto;
line-height: 2;
}
</style>

View File

@@ -7,11 +7,13 @@ export const useAuthStore = defineStore('auth', () => {
const nickname = ref<string | null>(null)
const userId = ref<number | null>(null)
const ready = ref(false)
const isNewUser = ref(false)
async function refresh() {
try {
const r = await checkLogin()
loggedIn.value = Boolean(r.logged_in)
isNewUser.value = Boolean(r.is_new_user)
if (r.user) {
userId.value = r.user.user_id
nickname.value = r.user.nickname ?? r.user.login_name ?? null
@@ -52,5 +54,5 @@ export const useAuthStore = defineStore('auth', () => {
await refresh()
}
return { loggedIn, nickname, userId, ready, refresh, login, logout, register }
return { loggedIn, nickname, userId, ready, isNewUser, refresh, login, logout, register }
})

View File

@@ -1,48 +1,62 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import OnboardingWizard from '@/components/onboarding/OnboardingWizard.vue'
const auth = useAuthStore()
const showOnboarding = ref(false)
const flaskOrigin = import.meta.env.VITE_FLASK_ORIGIN || ''
const generateUrl = flaskOrigin ? `${flaskOrigin}/` : '/'
onMounted(async () => {
if (!auth.ready) await auth.refresh()
showOnboarding.value = (auth as any).isNewUser ?? false
})
function openLegacyGenerate() {
window.location.href = generateUrl
}
function onOnboardingDone() {
showOnboarding.value = false
}
</script>
<template>
<div class="home">
<el-card shadow="never" class="hero">
<h1>Vue 3 前端试验田</h1>
<p class="lead">
与现有 Flask 后端并行开发开发时 Vite <code>/api</code><code>/poetry</code><code>/static</code> 代理到后端会话使用 Cookie
<code>withCredentials</code>
建议开发使用 <strong>127.0.0.1:3000</strong>经典版生成页也可从顶部 <strong>更多</strong> 菜单进入
</p>
<el-space wrap>
<el-button type="primary" @click="$router.push({ name: 'generate' })">Vue 提示词生成</el-button>
<el-button @click="$router.push({ name: 'optimization' })">提示词优化</el-button>
<el-button @click="$router.push({ name: 'resume-optimization' })">简历 / 求职信</el-button>
<el-button @click="openLegacyGenerate">经典生成页Flask</el-button>
<el-button @click="$router.push({ name: 'favorites' })">收藏列表</el-button>
<el-button @click="$router.push({ name: 'history' })">优化历史</el-button>
<el-button @click="$router.push({ name: 'meal-planning' })">饭菜规划</el-button>
<el-button @click="$router.push({ name: 'poetry' })">古诗词</el-button>
<el-button @click="$router.push({ name: 'android-tools' })">Android 工具</el-button>
<el-button @click="$router.push({ name: 'profile' })">个人资料</el-button>
</el-space>
</el-card>
<!-- 新用户引导 -->
<OnboardingWizard v-if="showOnboarding" @done="onOnboardingDone" />
<el-card shadow="never" class="status">
<template #header>会话状态</template>
<p v-if="!auth.ready">正在检测登录状态</p>
<template v-else>
<p v-if="auth.loggedIn">
已登录<strong>{{ auth.nickname || auth.userId }}</strong>
<div v-else>
<el-card shadow="never" class="hero">
<h1>提示词大师</h1>
<p class="lead">
智能 AI 提示词生成平台将简单想法变为专业 Prompt覆盖饭菜规划古诗词解析简历优化等 15 种场景
</p>
<p v-else>未登录收藏接口仍可能按访客策略返回数据取决于后端</p>
</template>
</el-card>
<el-space wrap>
<el-button type="primary" @click="$router.push({ name: 'generate' })">提示词生成</el-button>
<el-button @click="$router.push({ name: 'optimization' })">提示词优化</el-button>
<el-button @click="$router.push({ name: 'resume-optimization' })">简历优化</el-button>
<el-button @click="$router.push({ name: 'meal-planning' })">饭菜规划</el-button>
<el-button @click="$router.push({ name: 'poetry' })">古诗词</el-button>
<el-button @click="$router.push({ name: 'android-tools' })">Android 工具</el-button>
<el-button @click="$router.push({ name: 'favorites' })">收藏</el-button>
<el-button @click="$router.push({ name: 'history' })">历史</el-button>
<el-button @click="$router.push({ name: 'profile' })">个人资料</el-button>
</el-space>
</el-card>
<el-card shadow="never" class="status">
<template #header>会话状态</template>
<p v-if="!auth.ready">正在检测登录状态</p>
<template v-else>
<p v-if="auth.loggedIn">
已登录<strong>{{ auth.nickname || auth.userId }}</strong>
</p>
<p v-else>未登录 仍可使用核心功能登录后可收藏和查看历史</p>
</template>
</el-card>
</div>
</div>
</template>
@@ -71,13 +85,6 @@ function openLegacyGenerate() {
line-height: 1.65;
color: $text-secondary;
font-size: 0.95rem;
code {
font-size: 0.85em;
padding: 0.1em 0.35em;
background: $gray-100;
border-radius: 4px;
}
}
.status {