本阶段我的主要工作是把 StoryVerse 的前端从"可以运行的 Vue 项目"推进到"具备基本产品访问流程的前端应用"。项目的业务目标不是普通信息展示网站,而是一个围绕小说上传、剧情节点选择和实时角色扮演展开的交互式系统。因此前端基础建设不能只停留在页面跳转,而要提前考虑登录态保存、路由权限、统一请求封装、导航结构和后续页面之间的数据流转方式。这个阶段的代码主要集中在
frontend/src/router/index.ts
frontend/src/utils/axios.ts
frontend/src/stores/auth.ts
frontend/src/views/LoginView.vue
frontend/src/views/RegisterView.vue
frontend/src/components/AppNavBar.vue
我首先梳理了前端页面边界。系统至少需要登录页、注册页、首页、小说上传页、游戏读取页、剧情节点选择页和游玩页。不同页面的访问条件并不一样:登录和注册只应给未登录用户访问,首页、上传、开始游戏、游玩页则必须要求登录。基于这个判断,我在 Vue Router 中给路由增加了 meta 字段,用 requiresAuth 和 guestOnly 表示页面访问规则。这样后续新增页面时,不需要在每个页面组件里重复判断登录状态,而是在路由层统一处理。
关键代码如下:
router.beforeEach((to) => {
const { isLoggedIn } = useAuth()
if (to.meta.requiresAuth && !isLoggedIn.value) {
return {
name: 'login',
query: { redirect: to.fullPath },
}
}
if (to.meta.guestOnly && isLoggedIn.value) {
return { name: 'home' }
}
})
这段代码的意义不只是"未登录跳转登录页"。我在这里保留了 redirect 参数,原因是后续用户可能直接访问 /upload 或 /play/:sessionId,如果简单跳到登录页再回首页,会破坏用户原本的操作意图。保留 redirect 后,登录成功可以回到原目标页面。
登录态管理方面,我没有把用户信息只放在登录页组件里,而是抽成了 useAuth。实现时使用 Vue 的 ref 保存当前用户,同时把用户信息写入 localStorage。这样页面刷新后,登录态不会立即丢失。由于 localStorage 中的数据可能被用户手动修改,也可能因为旧版本结构变化导致不符合预期,所以读取时我做了类型校验和异常清理。
function readStoredUser(): AuthUser | null {
const raw = window.localStorage.getItem(STORAGE_KEY)
if (!raw) {
return null
}
try {
const parsed = JSON.parse(raw) as Partial<AuthUser>
if (typeof parsed.userId === 'number' && typeof parsed.username === 'string') {
return {
userId: parsed.userId,
username: parsed.username,
}
}
} catch {
window.localStorage.removeItem(STORAGE_KEY)
}
return null
}
这个实现解决了两个问题。第一,刷新页面后 router.beforeEach 依然能判断用户是否登录;第二,本地存储出现异常时不会让整个页面白屏,而是自动清理错误数据并回到未登录状态。这个细节在调试时很有价值,因为前端状态和浏览器缓存经常会影响测试结果。
接口层我封装了 axios 实例,并设置统一的 baseURL 和超时时间。项目后端默认运行在 http://127.0.0.1:8080,但前端不能把这个地址散落在每个页面里,否则后续切换环境会非常麻烦。因此我在 frontend/src/utils/axios.ts 中处理:
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8080',
timeout: 30000,
})
响应拦截器目前只返回 response.data,这让页面层不需要每次都写 response.data.data 或处理 Axios 的完整响应结构。这个封装也为后续统一错误提示、统一鉴权头、统一登录失效处理留下扩展位置。
登录页和注册页的开发,我重点处理了表单状态和用户反馈。登录页使用 reactive 保存用户名、密码,用 loading 控制按钮禁用,用 errorMessage 显示失败原因。提交前先做空值校验,避免明显无效请求打到后端。登录成功后调用 setUser 写入用户状态,然后根据路由中的 redirect 跳转。
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/'
await router.push(redirect)
这个细节和路由守卫形成闭环:用户访问受保护页面,被重定向到登录页,登录成功后再回到原页面。注册页则在注册成功后跳回登录页,并携带 registered 和 username 参数,便于后续扩展注册成功提示。
导航栏部分,我实现了 AppNavBar.vue,负责展示主导航、品牌名、当前用户和退出按钮。退出逻辑调用 logout() 清理本地登录态,然后跳转登录页。这让"登录、受保护路由、导航展示、退出登录"形成完整的访问闭环。
本阶段遇到的主要问题是:前端页面之间并不是孤立的,登录状态、路由跳转和接口调用会互相影响。如果每个页面都自己处理登录和请求,短期看能运行,后期会很难维护。因此我把用户态、请求实例和路由权限提前抽离出来。虽然这部分没有复杂算法,但它决定了后续页面能否稳定扩展。后面开发小说上传、首页、剧情选择、游玩页时,都直接复用了这个基础结构,没有再重复写登录判断和请求配置。
从工作量上看,本阶段完成了前端项目的基础访问框架,包括登录注册页面、登录态持久化、路由权限控制、统一请求层、导航栏和页面标题管理。更重要的是,我在这个阶段明确了前端的责任边界:前端不仅负责展示页面,还要负责把用户操作组织成连续、可恢复、可验证的业务流程。这为后续核心功能开发打下了基础。