状态管理概览
介绍
RuoYi-Plus-UniApp 前端项目采用 Pinia 作为官方状态管理方案,基于 Composition API 风格组织代码,提供类型安全的全局状态管理。Pinia 是 Vue 3 官方推荐的状态管理库,相比传统的 Vuex,它提供了更简洁的 API、更好的 TypeScript 支持、更灵活的模块化设计,以及更小的体积和更优的性能。
本项目的状态管理架构遵循模块化、类型安全、职责单一的设计原则,将应用状态按照业务领域划分为多个独立的 Store 模块,每个模块专注于特定的功能领域,通过统一的 API 提供状态访问和操作方法。
核心特性:
- Composition API 风格 - 使用
defineStore+ 组合式函数风格,代码结构清晰,易于维护和测试 - 完整的 TypeScript 支持 - 所有 Store 模块都提供完整的类型定义,实现编译时类型检查和智能提示
- 模块化设计 - 按照业务领域划分为 7 个核心模块,职责清晰,易于扩展
- 灵活的持久化策略 - 支持基于 localStorage 的状态持久化,可按需配置持久化策略
- 跨 Store 协作 - 支持在不同 Store 之间相互调用,实现复杂的业务逻辑
- 统一的错误处理 - 采用 Result 模式处理异步操作,统一错误处理流程
- 响应式数据流 - 基于 Vue 3 响应式系统,自动追踪依赖,高效更新视图
架构设计
Pinia 实例创建
项目在 src/stores/store.ts 中创建全局 Pinia 实例,在应用启动时注册到 Vue 应用:
// src/stores/store.ts
import { createPinia } from 'pinia'
// 创建一个 pinia 存储实例
const store = createPinia()
// 导出这个存储实例以便在应用程序的其他部分使用
export default store在 main.ts 中注册:
import { createApp } from 'vue'
import App from './App.vue'
import store from './stores/store'
const app = createApp(App)
app.use(store)
app.mount('#app')Store 模块划分
项目状态管理模块按照业务领域划分为以下 7 个核心模块:
stores/
├── store.ts # Pinia 实例创建
└── modules/
├── user.ts # 用户认证与权限
├── permission.ts # 路由权限管理
├── dict.ts # 字典数据管理
├── notice.ts # 通知消息管理
├── feature.ts # 系统功能配置管理
└── aiChat.ts # AI 聊天管理每个模块都遵循统一的代码组织结构:
/**
* 模块文档注释
* 包含功能说明、职责描述、使用示例
*/
/** 模块名称常量 */
const MODULE_NAME = 'moduleName'
/** 类型定义 */
export interface ModuleState {
// 状态类型定义
}
/** 导出 Store */
export const useModuleStore = defineStore(MODULE_NAME, () => {
// 响应式状态
const state = ref<ModuleState>()
// 计算属性
const getter = computed(() => {
return state.value
})
// 业务方法
const action = async (): Result<void> => {
// 异步操作实现
}
// 统一导出
return {
// 状态
state,
// 计算属性
getter,
// 方法
action
}
})设计原则
1. 模块化组织
每个 Store 负责独立的业务领域,职责单一,相互解耦。这种设计带来以下优势:
- 职责清晰: 每个模块只处理特定领域的状态和逻辑
- 易于维护: 修改某个功能时只需关注对应的 Store 模块
- 便于测试: 可以独立测试每个 Store 模块的功能
- 支持懒加载: 可以按需导入需要的 Store 模块
示例:
// user.ts - 只负责用户认证相关
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
const roles = ref([])
const permissions = ref([])
return { token, userInfo, roles, permissions }
})
// permission.ts - 只负责路由权限相关
export const usePermissionStore = defineStore('permission', () => {
const routes = ref([])
const sidebarRouters = ref([])
return { routes, sidebarRouters }
})2. Composition API 风格
采用 Composition API 风格定义 Store,使用 ref、computed、watch 等组合式 API,代码结构清晰,更接近 Vue 3 组件的开发方式:
export const useExampleStore = defineStore('example', () => {
// 响应式状态 - 使用 ref
const count = ref(0)
const name = ref('admin')
// 计算属性 - 使用 computed
const doubleCount = computed(() => count.value * 2)
const displayName = computed(() => `用户: ${name.value}`)
// 业务方法 - 普通函数
const increment = () => {
count.value++
}
const updateName = (newName: string) => {
name.value = newName
}
// 异步方法
const fetchData = async (): Result<void> => {
const [err, data] = await api.getData()
if (err) return [err, null]
// 更新状态
return [null, null]
}
// 统一导出
return {
// 状态
count,
name,
// 计算属性
doubleCount,
displayName,
// 方法
increment,
updateName,
fetchData
}
})3. TypeScript 类型支持
所有 Store 模块都提供完整的类型定义,确保类型安全和开发体验:
- 完整的类型定义和接口声明: 为状态、参数、返回值提供详细的类型注解
- 自动的类型推导和智能提示: 利用 TypeScript 的类型推导,提供准确的代码补全
- 编译时类型检查: 在编译阶段发现类型错误,减少运行时错误
示例:
/**
* 用户信息接口
*/
export interface SysUserVo {
userId: number
userName: string
nickName: string
email: string
phoneNumber: string
sex: string
avatar: string
status: string
delFlag: string
loginIp: string
loginDate: string
createTime: string
remark: string
}
/**
* 登录请求接口
*/
export interface LoginRequest {
userName: string
password: string
code: string
uuid: string
}
export const useUserStore = defineStore('user', () => {
// 使用类型注解
const userInfo = ref<SysUserVo | null>(null)
const roles = ref<Array<string>>([])
const permissions = ref<Array<string>>([])
// 参数和返回值都有类型
const loginUser = async (loginRequest: LoginRequest): Result<void> => {
// 实现
}
return {
userInfo,
roles,
permissions,
loginUser
}
})4. 持久化策略
项目采用灵活的持久化策略,不同的数据根据业务需求选择合适的持久化方式:
- Token 管理: 使用专门的
useToken工具类管理,支持 localStorage 持久化 - 用户信息: 登录后从服务器获取,不持久化,通过 Token 重新获取
- 主题配置: 使用
localCache工具持久化到 localStorage - 应用状态: 部分状态(如侧边栏折叠状态)自动同步到 localStorage
- 临时数据: 字典、通知等数据仅存储在内存中,页面刷新后重新加载
示例:
// Token 持久化 - 使用专门的工具类
import { useToken } from '@/composables/useToken'
export const useUserStore = defineStore('user', () => {
const tokenUtils = useToken()
const token = ref(tokenUtils.getToken())
const loginUser = async (loginRequest: LoginRequest): Result<void> => {
const [err, data] = await userLogin(loginRequest)
if (err) return [err, null]
// 保存到 localStorage 和 store
tokenUtils.setToken(data.access_token, data.expire_in)
token.value = data.access_token
return [null, null]
}
return { token, loginUser }
})5. Result 模式错误处理
项目采用 Result 模式处理异步操作,统一错误处理流程,避免 try-catch 嵌套:
/**
* Result 类型定义
* [error, data] 元组
* - error 为 null 时表示成功,data 包含结果
* - error 不为 null 时表示失败,data 为 null
*/
type Result<T> = [Error | null, T | null]
// 在 Store 中使用
export const useUserStore = defineStore('user', () => {
const fetchUserInfo = async (): Result<void> => {
// 调用 API,返回 Result 类型
const [err, data] = await getUserInfo()
// 错误处理
if (err) {
console.error('获取用户信息失败:', err)
return [err, null]
}
// 成功处理
userInfo.value = data.user
roles.value = data.roles
permissions.value = data.permissions
return [null, null]
}
return { fetchUserInfo }
})
// 在组件中使用
const handleGetUserInfo = async () => {
const [err] = await userStore.fetchUserInfo()
if (err) {
ElMessage.error('获取用户信息失败')
return
}
ElMessage.success('获取成功')
}核心 Store 模块详解
1. User Store (用户认证与权限)
用户认证管理模块,负责用户登录、注销、用户信息获取、权限管理等核心认证功能。
状态定义
export const useUserStore = defineStore('user', () => {
/**
* 用户令牌
* 用户登录后的访问令牌,用于 API 请求认证
*/
const token = ref(tokenUtils.getToken())
/**
* 用户基本信息
* 用户的基本信息(账号、昵称、头像等)
*/
const userInfo = ref<SysUserVo | null>(null)
/**
* 用户角色编码集合
* 用于判断路由权限和功能权限
*/
const roles = ref<Array<string>>([])
/**
* 用户权限编码集合
* 用于判断按钮权限等细粒度权限控制
*/
const permissions = ref<Array<string>>([])
return {
token,
userInfo,
roles,
permissions
}
})核心方法
用户登录
const loginUser = async (loginRequest: LoginRequest): Result<void> => {
const [err, data] = await userLogin(loginRequest)
if (err) {
return [err, null]
}
// 保存 token 到 localStorage 和 store
tokenUtils.setToken(data.access_token, data.expire_in)
token.value = data.access_token
return [null, null]
}获取用户信息
const fetchUserInfo = async (): Result<void> => {
const [err, data] = await getUserInfo()
if (err) {
return [err, null]
}
const user = data.user
// 处理用户头像
if (!user.avatar) {
user.avatar = defAva
}
// 设置用户基本信息
userInfo.value = user
// 设置角色和权限
roles.value = data.roles || []
permissions.value = data.permissions || []
return [null, null]
}用户注销
const logoutUser = async (): Result<void> => {
// 调用注销 API
const [err] = await userLogout()
// 清除状态
token.value = ''
userInfo.value = null
roles.value = []
permissions.value = []
// 移除 localStorage 中的 token
tokenUtils.removeToken()
return [err, null]
}更新用户头像
const updateAvatar = (avatarUrl: string): void => {
if (userInfo.value) {
userInfo.value.avatar = avatarUrl
}
}使用示例
<template>
<div class="user-info">
<el-avatar :src="avatar" />
<span>{{ nickname }}</span>
<el-button @click="handleLogout">退出登录</el-button>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/modules/user'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
// 获取用户信息
const nickname = computed(() => userStore.userInfo?.nickName || '')
const avatar = computed(() => userStore.userInfo?.avatar || '')
// 退出登录
const handleLogout = async () => {
const [err] = await userStore.logoutUser()
if (err) {
ElMessage.error('退出失败')
return
}
// 跳转到登录页
router.push('/login')
}
// 组件挂载时获取用户信息
onMounted(async () => {
if (userStore.token && !userStore.roles.length) {
await userStore.fetchUserInfo()
}
})
</script>2. Permission Store (路由权限管理)
权限路由管理模块,负责从后端获取路由配置,动态构建应用路由结构,并提供多布局路由管理功能。
状态定义
export const usePermissionStore = defineStore('permission', () => {
/**
* 路由记录
* 所有路由配置的集合,包含静态路由和动态添加的路由
*/
const routes = ref<RouteRecordRaw[]>([])
/**
* 动态添加的路由
* 从后端获取并动态添加的路由配置
*/
const addRoutes = ref<RouteRecordRaw[]>([])
/**
* 默认路由
* 用于基础布局的路由配置
*/
const defaultRoutes = ref<RouteRecordRaw[]>([])
/**
* 顶部栏路由
* 用于顶部导航栏显示的路由配置
*/
const topbarRouters = ref<RouteRecordRaw[]>([])
/**
* 侧边栏路由
* 用于侧边栏菜单显示的路由配置
*/
const sidebarRouters = ref<RouteRecordRaw[]>([])
return {
routes,
addRoutes,
defaultRoutes,
topbarRouters,
sidebarRouters
}
})核心方法
生成路由
从后端获取路由数据并处理成可用的路由配置:
const generateRoutes = async (): Result<RouteRecordRaw[]> => {
// 从后端 API 获取路由数据
const [err, data] = await getRouters()
if (err) {
return [err, null]
}
// 深拷贝路由数据用于不同处理场景
const sdata = JSON.parse(JSON.stringify(data))
const rdata = JSON.parse(JSON.stringify(data))
const defaultData = JSON.parse(JSON.stringify(data))
// 处理不同场景的路由格式
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, undefined, true)
const defaultRoutes = filterAsyncRouter(defaultData)
// 处理动态权限路由
const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
asyncRoutes.forEach((route) => {
router.addRoute(route)
})
// 设置各类路由到 store
setRoutes(rewriteRoutes)
setSidebarRouters(constantRoutes.concat(sidebarRoutes))
setDefaultRoutes(sidebarRoutes)
setTopbarRoutes(defaultRoutes)
// 路由 name 重复检查,避免 404 问题
duplicateRouteChecker(asyncRoutes, sidebarRoutes)
return [null, rewriteRoutes]
}过滤异步路由
将后台传来的路由字符串转换为组件对象:
const filterAsyncRouter = (
asyncRouterMap: RouteRecordRaw[],
lastRouter?: RouteRecordRaw,
type = false
): RouteRecordRaw[] => {
return asyncRouterMap.filter((route) => {
if (type && route.children) {
route.children = filterChildren(route.children, undefined)
}
// Layout ParentView 组件特殊处理
if (route.component?.toString() === 'Layout') {
route.component = Layout
} else if (route.component?.toString() === 'ParentView') {
route.component = ParentView
} else if (route.component?.toString() === 'InnerLink') {
route.component = InnerLink
} else {
route.component = loadView(route.component, route.name as string)
}
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type)
} else {
delete route.children
delete route.redirect
}
return true
})
}动态路由权限过滤
遍历动态路由,验证是否具备权限:
const filterDynamicRoutes = (routes: RouteRecordRaw[]): RouteRecordRaw[] => {
const res: RouteRecordRaw[] = []
const { hasPermission, hasRole } = useAuth()
routes.forEach((route) => {
if (route.permissions) {
// 检查是否有任一所需权限
if (hasPermission(route.permissions)) {
res.push(route)
}
} else if (route.roles) {
// 检查是否有任一所需角色
if (hasRole(route.roles)) {
res.push(route)
}
}
})
return res
}使用示例
<template>
<el-menu :default-active="activeMenu" mode="horizontal">
<sidebar-item
v-for="route in topbarRouters"
:key="route.path"
:item="route"
/>
</el-menu>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/modules/permission'
import { useRoute } from 'vue-router'
const permissionStore = usePermissionStore()
const route = useRoute()
// 获取顶部导航栏路由
const topbarRouters = computed(() => permissionStore.getTopbarRoutes())
// 当前激活的菜单项
const activeMenu = computed(() => route.path)
// 在路由守卫中生成路由
import { useRouter } from 'vue-router'
const router = useRouter()
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
if (userStore.token) {
// 已登录,检查是否已生成路由
if (permissionStore.routes.length === 0) {
// 获取用户信息
await userStore.fetchUserInfo()
// 生成路由
const [err] = await permissionStore.generateRoutes()
if (err) {
ElMessage.error('获取路由失败')
next('/login')
return
}
// 重定向到目标路由
next({ ...to, replace: true })
} else {
next()
}
} else {
// 未登录,跳转登录页
next('/login')
}
})
</script>3. Dict Store (字典数据管理)
字典数据管理模块,提供统一的字典数据存储、访问和转换功能,用于下拉选项、数据映射等场景。
状态定义
export const useDictStore = defineStore('dict', () => {
/**
* 字典数据集合
* 使用 Map 存储多个字典数据,key 为字典类型,value 为字典选项数组
* @example
* {
* 'sys_user_gender': [
* { label: '男', value: '0' ... },
* { label: '女', value: '1' ... }
* ],
* 'sys_enable_status': [
* { label: '正常', value: '0' ... },
* { label: '停用', value: '1' ... }
* ]
* }
*/
const dict = ref<Map<string, DictItem[]>>(new Map())
return { dict }
})核心方法
获取字典
const getDict = (key: string): DictItem[] | null => {
if (!key) {
return null
}
return dict.value.get(key) || null
}设置字典
const setDict = (key: string, value: DictItem[]): boolean => {
if (!key) {
return false
}
try {
dict.value.set(key, value)
return true
} catch (e) {
console.error('设置字典时发生错误:', e)
return false
}
}获取字典标签
根据字典类型或字典数据和值获取标签:
const getDictLabel = (
keyOrData: string | Ref<DictItem[]> | DictItem[],
value: string | number
): string => {
let dictData: Ref<DictItem[]> | undefined
if (typeof keyOrData === 'string') {
dictData = ref(getDict(keyOrData))
} else if (isRef(keyOrData)) {
dictData = keyOrData
} else {
dictData = ref(keyOrData)
}
if (!dictData) return ''
const item = dictData.value.find((item) => item.value === String(value))
return item ? item.label : ''
}批量获取字典标签
const getDictLabels = (
keyOrData: string | Ref<DictItem[]>,
values: (string | number)[]
): string[] => {
if (!values || values.length === 0) return []
let dictData: Ref<DictItem[]> | undefined
if (typeof keyOrData === 'string') {
dictData = ref(getDict(keyOrData))
} else {
dictData = keyOrData
}
if (!dictData) return values.map(() => '')
return values.map((value) => {
const item = dictData.value.find((item) => item.value === value)
return item ? item.label : ''
})
}使用示例
<template>
<el-form :model="form">
<el-form-item label="性别">
<el-select v-model="form.gender">
<el-option
v-for="item in genderDict"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<span>{{ statusLabel }}</span>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { useDictStore } from '@/stores/modules/dict'
import { getDict } from '@/api/system/dict/data'
const dictStore = useDictStore()
const form = ref({
gender: '0',
status: '1'
})
// 获取性别字典
const genderDict = computed(() => dictStore.getDict('sys_user_gender'))
// 获取状态标签
const statusLabel = computed(() =>
dictStore.getDictLabel('sys_enable_status', form.value.status)
)
// 初始化字典数据
onMounted(async () => {
// 加载性别字典
const [err1, genderData] = await getDict('sys_user_gender')
if (!err1) {
dictStore.setDict('sys_user_gender', genderData)
}
// 加载状态字典
const [err2, statusData] = await getDict('sys_enable_status')
if (!err2) {
dictStore.setDict('sys_enable_status', statusData)
}
})
</script>4. Notice Store (通知消息管理)
通知管理中心模块,提供系统通知的集中存储和处理能力。
状态定义
/**
* 通知项接口
*/
interface NoticeItem {
title?: string
read: boolean
message: any
time: string
}
export const useNoticeStore = defineStore('notice', () => {
/**
* 通知列表
*/
const notices = ref<NoticeItem[]>([])
return { notices }
})核心方法
添加通知
const addNotice = (notice: NoticeItem): void => {
notices.value.push(notice)
}移除通知
const removeNotice = (notice: NoticeItem): void => {
const index = notices.value.indexOf(notice)
if (index !== -1) {
notices.value.splice(index, 1)
}
}全部标记为已读
const readAll = () => {
notices.value.forEach((item: NoticeItem) => {
item.read = true
})
}清空所有通知
const clearNotice = (): void => {
notices.value = []
}使用示例
<template>
<el-dropdown>
<el-badge :value="unreadCount">
<el-icon><Bell /></el-icon>
</el-badge>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="notice in noticeStore.notices"
:key="notice.time"
@click="handleNoticeClick(notice)"
>
<div :class="{ 'is-read': notice.read }">
<div class="title">{{ notice.title }}</div>
<div class="message">{{ notice.message }}</div>
<div class="time">{{ notice.time }}</div>
</div>
</el-dropdown-item>
<el-dropdown-item divided>
<el-button link @click="noticeStore.readAll()">全部已读</el-button>
<el-button link @click="noticeStore.clearNotice()">清空通知</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useNoticeStore } from '@/stores/modules/notice'
const noticeStore = useNoticeStore()
// 未读通知数量
const unreadCount = computed(() =>
noticeStore.notices.filter(n => !n.read).length
)
// 点击通知
const handleNoticeClick = (notice: NoticeItem) => {
notice.read = true
// 处理通知点击逻辑
}
// 模拟添加通知
const addNewNotice = () => {
noticeStore.addNotice({
title: '系统通知',
read: false,
message: '您有一条新消息',
time: new Date().toLocaleString()
})
}
</script>5. Feature Store (系统功能配置管理)
系统功能配置管理模块,提供统一的功能开关管理,支持从服务端动态获取功能配置。
状态定义
export const useFeatureStore = defineStore('feature', () => {
/**
* 系统功能配置
* 存储所有功能的启用状态
*/
const features = ref<SystemFeature>({
langchain4jEnabled: false,
websocketEnabled: false,
sseEnabled: false,
openApiEnabled: false,
openApiAccessMode: 'ALL',
openApiAllowedRoles: []
})
/**
* 配置是否已初始化
*/
const initialized = ref(false)
return {
features,
initialized
}
})核心方法
初始化功能配置
从服务端获取功能配置,应在应用启动时调用一次:
const initFeatures = async (): Promise<void> => {
if (initialized.value) {
return
}
const [err, data] = await getSystemFeatures()
if (err) {
features.value = {
langchain4jEnabled: false,
websocketEnabled: false,
sseEnabled: false,
openApiEnabled: false,
openApiAccessMode: 'ALL',
openApiAllowedRoles: []
}
} else {
features.value = data
}
initialized.value = true
}检查 OpenAPI 访问权限
检查当前用户是否可以使用开放 API:
const canUseOpenApi = (userRoles: string[]): boolean => {
if (!features.value.openApiEnabled) {
return false
}
const mode = features.value.openApiAccessMode
const allowedRoles = features.value.openApiAllowedRoles || []
switch (mode) {
case 'ALL':
return true
case 'SUPER_ADMIN':
return userRoles.includes('superadmin')
case 'ADMIN':
return userRoles.includes('superadmin') || userRoles.includes('admin')
case 'ROLES':
if (allowedRoles.length === 0) {
return false
}
return userRoles.some((role) => allowedRoles.includes(role))
default:
return false
}
}使用示例
<template>
<div>
<!-- 根据功能开关显示不同内容 -->
<div v-if="featureStore.features.websocketEnabled">
<websocket-chat />
</div>
<div v-if="canShowOpenApi">
<open-api-docs />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import { useFeatureStore } from '@/stores/modules/feature'
import { useUserStore } from '@/stores/modules/user'
const featureStore = useFeatureStore()
const userStore = useUserStore()
// 检查是否可以显示 OpenAPI 文档
const canShowOpenApi = computed(() =>
featureStore.canUseOpenApi(userStore.roles)
)
// 应用启动时初始化功能配置
onMounted(async () => {
await featureStore.initFeatures()
})
</script>6. AI Chat Store (AI 聊天管理)
AI 聊天数据管理模块,提供统一的会话管理和消息处理功能,支持 WebSocket 实时通信和流式响应。
状态定义
/**
* AI 聊天消息接口
*/
export interface AiChatMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: number
tokenUsage?: {
promptTokens: number
completionTokens: number
totalTokens: number
}
references?: any[]
status?: 'sending' | 'streaming' | 'complete' | 'error'
error?: string
}
/**
* AI 聊天会话接口
*/
export interface AiChatSession {
id: string
title: string
messages: AiChatMessage[]
createdAt: number
updatedAt: number
provider?: string
modelName?: string
}
export const useAiChatStore = defineStore('aiChat', () => {
/**
* 会话集合
* 使用 Map 存储多个会话,key 为会话 ID,value 为会话对象
*/
const sessions = ref<Map<string, AiChatSession>>(new Map())
/**
* 当前活跃的会话 ID
*/
const currentSessionId = ref<string | null>(null)
/**
* 当前正在流式生成的消息 ID
*/
const streamingMessageId = ref<string | null>(null)
/**
* 流式内容缓冲区
*/
const streamContentBuffer = ref<string>('')
return {
sessions,
currentSessionId,
streamingMessageId,
streamContentBuffer
}
})计算属性
/**
* 获取当前会话
*/
const currentSession = computed((): AiChatSession | null => {
if (!currentSessionId.value) return null
return sessions.value.get(currentSessionId.value) || null
})
/**
* 获取当前会话的消息列表
*/
const currentMessages = computed((): AiChatMessage[] => {
return currentSession.value?.messages || []
})
/**
* 是否正在生成中
*/
const isGenerating = computed((): boolean => {
return streamingMessageId.value !== null
})
/**
* 获取所有会话列表(按更新时间倒序)
*/
const sessionList = computed((): AiChatSession[] => {
return Array.from(sessions.value.values())
.sort((a, b) => b.updatedAt - a.updatedAt)
})核心方法
创建新会话
const createSession = (options?: {
title?: string
provider?: string
modelName?: string
}): string => {
const sessionId = generateSessionId()
const now = Date.now()
const newSession: AiChatSession = {
id: sessionId,
title: options?.title || `新对话 ${new Date().toLocaleString()}`,
messages: [],
createdAt: now,
updatedAt: now,
provider: options?.provider || 'deepseek',
modelName: options?.modelName || 'deepseek-chat'
}
sessions.value.set(sessionId, newSession)
currentSessionId.value = sessionId
return sessionId
}发送消息到 AI
const sendMessage = (
content: string,
options?: {
sessionId?: string
provider?: string
modelName?: string
systemPrompt?: string
temperature?: number
maxTokens?: number
}
): boolean => {
// 确保有会话
let sessionId = options?.sessionId || currentSessionId.value
if (!sessionId) {
sessionId = createSession()
}
const session = sessions.value.get(sessionId)
if (!session) {
console.error(`会话不存在: ${sessionId}`)
return false
}
// 添加用户消息
const userMessage: AiChatMessage = {
id: generateMessageId(),
role: 'user',
content: content,
timestamp: Date.now(),
status: 'complete'
}
session.messages.push(userMessage)
// 添加一个空的助手消息,用于接收流式内容
const assistantMessage: AiChatMessage = {
id: '',
role: 'assistant',
content: '',
timestamp: Date.now(),
status: 'sending'
}
session.messages.push(assistantMessage)
streamingMessageId.value = sessionId
streamContentBuffer.value = ''
// 更新会话时间
session.updatedAt = Date.now()
// 通过 WebSocket 发送消息
const request = {
type: 'ai_chat',
sessionId: sessionId,
message: content,
provider: options?.provider || session.provider || 'deepseek',
modelName: options?.modelName || session.modelName || 'deepseek-chat',
mode: 'CONTINUOUS',
systemPrompt: options?.systemPrompt,
temperature: options?.temperature,
maxTokens: options?.maxTokens
}
const success = webSocket.send(request)
if (!success) {
assistantMessage.status = 'error'
assistantMessage.error = 'WebSocket 未连接,无法发送消息'
streamingMessageId.value = null
return false
}
return true
}WebSocket 回调 - 追加流式内容
const appendStreamContent = (
sessionId: string,
messageId: string,
content: string
): void => {
const session = sessions.value.get(sessionId)
if (!session) {
console.error('AI聊天: 找不到 session')
return
}
// 查找对应的消息
let message = session.messages.find((msg) => msg.id === messageId)
if (!message) {
// 如果找不到,使用最后一条助手消息
const assistantMessages = session.messages.filter(
(msg) => msg.role === 'assistant'
)
message = assistantMessages[assistantMessages.length - 1]
// 回填 messageId
if (message && !message.id) {
message.id = messageId
}
}
if (message) {
message.content += content
message.status = 'streaming'
// 强制触发响应式更新
sessions.value.set(sessionId, { ...session })
} else {
// 追加到缓冲区
streamContentBuffer.value += content
}
}使用示例
<template>
<div class="ai-chat">
<!-- 会话列表 -->
<div class="session-list">
<el-button @click="createNewSession">新建对话</el-button>
<div
v-for="session in aiChatStore.sessionList"
:key="session.id"
:class="{ active: session.id === aiChatStore.currentSessionId }"
@click="switchToSession(session.id)"
>
{{ session.title }}
</div>
</div>
<!-- 消息列表 -->
<div class="message-list">
<div
v-for="message in aiChatStore.currentMessages"
:key="message.id"
:class="['message', message.role]"
>
<div class="content">{{ message.content }}</div>
<div v-if="message.status === 'streaming'" class="streaming-indicator">
<el-icon class="is-loading"><Loading /></el-icon>
</div>
</div>
</div>
<!-- 输入框 -->
<div class="input-area">
<el-input
v-model="inputContent"
type="textarea"
:disabled="aiChatStore.isGenerating"
@keydown.enter="handleSend"
/>
<el-button
type="primary"
:loading="aiChatStore.isGenerating"
@click="handleSend"
>
发送
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useAiChatStore } from '@/stores/modules/aiChat'
const aiChatStore = useAiChatStore()
const inputContent = ref('')
// 创建新会话
const createNewSession = () => {
aiChatStore.createSession({
title: '新对话',
provider: 'deepseek',
modelName: 'deepseek-chat'
})
}
// 切换会话
const switchToSession = (sessionId: string) => {
aiChatStore.switchSession(sessionId)
}
// 发送消息
const handleSend = () => {
if (!inputContent.value.trim() || aiChatStore.isGenerating) {
return
}
aiChatStore.sendMessage(inputContent.value, {
temperature: 0.7,
maxTokens: 2000
})
inputContent.value = ''
}
// 初始化
onMounted(() => {
if (aiChatStore.sessionList.length === 0) {
createNewSession()
}
})
</script>使用规范
1. 在组件中使用 Store
在 Vue 组件中导入并使用 Store 模块:
import { useUserStore } from '@/stores/modules/user'
export default defineComponent({
setup() {
const userStore = useUserStore()
// 访问状态
console.log(userStore.userInfo)
// 调用方法
await userStore.fetchUserInfo()
// 使用计算属性访问状态
const nickname = computed(() => userStore.userInfo?.nickName || '')
return { nickname }
}
})使用 <script setup> 语法:
<script lang="ts" setup>
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
// 访问状态
const userInfo = computed(() => userStore.userInfo)
// 调用方法
const handleFetchUserInfo = async () => {
await userStore.fetchUserInfo()
}
</script>2. 跨 Store 协作
在一个 Store 中使用另一个 Store:
import { useUserStore } from '@/stores/modules/user'
export const usePermissionStore = defineStore('permission', () => {
const generateRoutes = async (): Result<RouteRecordRaw[]> => {
// 使用 useAuth Composable 获取权限信息
const { hasPermission, hasRole } = useAuth()
// 过滤路由
routes.forEach((route) => {
if (route.permissions && !hasPermission(route.permissions)) {
// 移除无权限路由
}
})
return [null, routes]
}
return { generateRoutes }
})在组件中组合多个 Store:
<script lang="ts" setup>
import { useUserStore } from '@/stores/modules/user'
import { usePermissionStore } from '@/stores/modules/permission'
import { useDictStore } from '@/stores/modules/dict'
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const dictStore = useDictStore()
// 组合使用多个 Store
const initApp = async () => {
// 1. 获取用户信息
await userStore.fetchUserInfo()
// 2. 生成路由
await permissionStore.generateRoutes()
// 3. 加载字典
const [err, dictData] = await getDict('sys_user_gender')
if (!err) {
dictStore.setDict('sys_user_gender', dictData)
}
}
</script>3. 异步操作规范
统一使用 Result 类型处理异步结果,避免 try-catch 嵌套:
// ✅ 推荐写法
const fetchData = async (): Result<Data> => {
const [err, data] = await api.getData()
if (err) {
console.error('获取数据失败:', err)
return [err, null]
}
// 处理数据
state.value = data
return [null, data]
}
// 在组件中使用
const handleFetch = async () => {
const [err, data] = await store.fetchData()
if (err) {
ElMessage.error('操作失败')
return
}
ElMessage.success('操作成功')
}// ❌ 不推荐写法
const fetchData = async () => {
try {
const data = await api.getData()
state.value = data
return data
} catch (error) {
console.error('获取数据失败:', error)
throw error
}
}
// 组件中需要再次 try-catch
const handleFetch = async () => {
try {
await store.fetchData()
ElMessage.success('操作成功')
} catch (error) {
ElMessage.error('操作失败')
}
}4. 响应式状态解构
由于 Pinia 的 Store 是响应式对象,直接解构会丢失响应性。使用 storeToRefs 保持响应性:
// ❌ 错误写法 - 丢失响应性
const { userInfo, roles } = useUserStore()
// ✅ 正确写法 - 保持响应性
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo, roles } = storeToRefs(userStore)
// 方法可以直接解构
const { fetchUserInfo, logoutUser } = userStore5. Store 重置
Pinia 提供 $reset 方法重置 Store 到初始状态:
const userStore = useUserStore()
// 重置 Store
userStore.$reset()对于 Composition API 风格的 Store,需要手动实现重置逻辑:
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
const roles = ref([])
const permissions = ref([])
// 重置方法
const reset = () => {
token.value = ''
userInfo.value = null
roles.value = []
permissions.value = []
}
return {
token,
userInfo,
roles,
permissions,
reset
}
})最佳实践
1. 避免直接修改状态
通过定义的方法修改状态,而不是直接修改:
// ❌ 不推荐 - 直接修改
userStore.userInfo.nickName = '新昵称'
// ✅ 推荐 - 通过方法修改
const updateNickName = (nickName: string) => {
if (userInfo.value) {
userInfo.value.nickName = nickName
}
}2. 合理使用计算属性
派生状态使用 computed,避免冗余数据:
export const useUserStore = defineStore('user', () => {
const userInfo = ref<SysUserVo | null>(null)
// ✅ 使用计算属性
const displayName = computed(() => {
return userInfo.value?.nickName || userInfo.value?.userName || '未知用户'
})
const isAdmin = computed(() => {
return roles.value.includes('admin')
})
return {
userInfo,
displayName,
isAdmin
}
})3. 异步操作错误处理
统一的错误处理机制,提供友好的错误提示:
const fetchUserInfo = async (): Result<void> => {
const [err, data] = await getUserInfo()
if (err) {
console.error('获取用户信息失败:', err)
ElMessage.error(err.message || '获取用户信息失败')
return [err, null]
}
userInfo.value = data.user
roles.value = data.roles
permissions.value = data.permissions
return [null, null]
}4. 模块职责清晰
不跨界处理其他模块的业务,保持职责单一:
// ❌ 不推荐 - 在 user store 中处理路由
export const useUserStore = defineStore('user', () => {
const loginUser = async (loginRequest: LoginRequest): Result<void> => {
const [err, data] = await userLogin(loginRequest)
if (err) return [err, null]
// ❌ 不应该在这里处理路由
const permissionStore = usePermissionStore()
await permissionStore.generateRoutes()
router.push('/dashboard')
return [null, null]
}
})
// ✅ 推荐 - 在组件或路由守卫中协调
const handleLogin = async () => {
const [err] = await userStore.loginUser(loginForm)
if (err) {
ElMessage.error('登录失败')
return
}
// 在组件中协调多个 Store
await permissionStore.generateRoutes()
router.push('/dashboard')
}5. 类型定义完整
确保 TypeScript 类型覆盖所有状态和方法:
// ✅ 完整的类型定义
export interface LoginRequest {
userName: string
password: string
code: string
uuid: string
}
export interface SysUserVo {
userId: number
userName: string
nickName: string
email: string
phoneNumber: string
avatar: string
status: string
}
export const useUserStore = defineStore('user', () => {
const userInfo = ref<SysUserVo | null>(null)
const roles = ref<Array<string>>([])
const loginUser = async (loginRequest: LoginRequest): Result<void> => {
// 实现
}
return {
userInfo,
roles,
loginUser
}
})6. 合理使用持久化
根据数据特性选择合适的持久化策略:
- 敏感数据: Token 使用专门的工具类管理,支持加密存储
- 用户偏好: 主题、语言等配置持久化到 localStorage
- 临时状态: 字典、通知等数据不持久化,页面刷新后重新加载
- 大量数据: 考虑使用 IndexedDB 而不是 localStorage
// Token 持久化
const tokenUtils = useToken()
tokenUtils.setToken(token, expireIn)
// 主题配置持久化
import { localCache } from '@/utils/cache'
localCache.setCache('theme', themeConfig)7. Store 模块命名规范
- Store 文件: 小写连字符,如
user.ts、permission.ts - Store 名称: 小写连字符,如
'user'、'permission' - Composable: 驼峰命名,如
useUserStore、usePermissionStore
// ✅ 推荐命名
const USER_MODULE = 'user'
export const useUserStore = defineStore(USER_MODULE, () => {
// ...
})
// ❌ 不推荐命名
const userModule = 'User'
export const UserStore = defineStore(userModule, () => {
// ...
})常见问题
1. Store 状态丢失响应性
问题描述:
直接解构 Store 导致状态丢失响应性,页面不更新。
原因分析:
Pinia 的 Store 是响应式对象,直接解构会丢失响应性。
解决方案:
使用 storeToRefs 保持响应性:
// ❌ 错误 - 丢失响应性
const { userInfo } = useUserStore()
// ✅ 正确 - 保持响应性
import { storeToRefs } from 'pinia'
const { userInfo } = storeToRefs(useUserStore())2. 跨 Store 调用导致循环依赖
问题描述:
Store A 导入 Store B,Store B 又导入 Store A,导致循环依赖错误。
原因分析:
TypeScript/JavaScript 模块系统不支持循环依赖。
解决方案:
在方法内部按需导入,而不是在模块顶部导入:
// ❌ 错误 - 顶部导入导致循环依赖
import { usePermissionStore } from './permission'
export const useUserStore = defineStore('user', () => {
const permissionStore = usePermissionStore()
// ...
})
// ✅ 正确 - 方法内按需导入
export const useUserStore = defineStore('user', () => {
const afterLogin = () => {
// 在需要时导入
const permissionStore = usePermissionStore()
permissionStore.generateRoutes()
}
return { afterLogin }
})3. Store 在路由守卫中无法访问
问题描述:
在路由守卫中访问 Store 时报错或获取不到数据。
原因分析:
路由守卫执行时 Pinia 实例可能还未完全初始化。
解决方案:
确保在 Vue 应用创建后再设置路由守卫,或者在守卫内部访问 Store:
// ✅ 正确 - 在守卫内部访问
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (userStore.token) {
// 已登录逻辑
} else {
// 未登录逻辑
}
next()
})4. 异步操作后状态未更新
问题描述:
调用 Store 的异步方法后,状态没有更新,页面不刷新。
原因分析:
可能是直接修改了对象属性,而不是替换整个响应式对象。
解决方案:
确保使用 .value 更新 ref,或使用响应式 API:
// ❌ 可能无效
const updateUserInfo = (newInfo: SysUserVo) => {
Object.assign(userInfo.value, newInfo)
}
// ✅ 推荐
const updateUserInfo = (newInfo: SysUserVo) => {
userInfo.value = { ...userInfo.value, ...newInfo }
}
// ✅ 或者直接替换
const updateUserInfo = (newInfo: SysUserVo) => {
userInfo.value = newInfo
}5. Store 数据在页面刷新后丢失
问题描述:
页面刷新后 Store 中的数据全部丢失。
原因分析:
默认情况下 Pinia Store 的数据存储在内存中,页面刷新后会重置。
解决方案:
使用持久化插件或手动实现持久化:
// 方案 1: 使用 pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', () => {
// ...
}, {
persist: true
})
// 方案 2: 手动实现持久化
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const setToken = (newToken: string) => {
token.value = newToken
localStorage.setItem('token', newToken)
}
return { token, setToken }
})总结
RuoYi-Plus-UniApp 项目的状态管理架构基于 Pinia,遵循模块化、类型安全、职责单一的设计原则。通过合理的 Store 模块划分、统一的错误处理机制、灵活的持久化策略,为应用提供了可靠的全局状态管理能力。
在实际开发中,应遵循以下原则:
- 模块化设计 - 按业务领域划分 Store 模块
- 类型安全 - 提供完整的 TypeScript 类型定义
- 职责单一 - 每个 Store 只负责特定领域
- 统一错误处理 - 使用 Result 模式处理异步操作
- 合理持久化 - 根据数据特性选择持久化策略
- 响应式解构 - 使用
storeToRefs保持响应性 - 计算属性优化 - 派生状态使用
computed
通过遵循这些规范和最佳实践,可以构建出高质量、易维护的前端应用。
