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:
@@ -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'])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface CheckLoginResponse {
|
||||
success: boolean
|
||||
logged_in: boolean
|
||||
is_new_user?: boolean
|
||||
user?: {
|
||||
user_id: number
|
||||
nickname?: string | null
|
||||
|
||||
179
vue-app/src/components/onboarding/OnboardingWizard.vue
Normal file
179
vue-app/src/components/onboarding/OnboardingWizard.vue
Normal 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>
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user