Pinia 作为 Vue3 官方状态管理库,支持模块化拆分、异步操作、状态持久化等高级功能,解决了 Vuex 的复杂语法问题,以下是企业级项目中的进阶用法:
(1)模块化状态管理(按业务拆分 Store)
按业务领域拆分 Store(如用户、购物车、商品),使状态管理更清晰,便于维护。
// 1. 用户Store(src/store/modules/userStore.js)
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi, logoutApi } from '@/api/user' // 模拟API请求
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '', // 从localStorage初始化
userInfo: JSON.parse(localStorage.getItem('userInfo')) || {},
isLogin: !!localStorage.getItem('token') // 布尔值:是否登录
}),
getters: {
// 计算用户是否为管理员
isAdmin: (state) => state.userInfo.role === 'admin',
// 获取用户昵称(优先显示昵称,无则显示用户名)
userNickname: (state) => state.userInfo.nickname || state.userInfo.username
},
actions: {
// 异步登录:调用API获取token和用户信息
async login(userData) {
try {
const { data } = await loginApi(userData) // 调用登录API
const { token } = data
// 存储token
this.token = token
localStorage.setItem('token', token)
// 获取用户信息
await this.fetchUserInfo()
// 更新登录状态
this.isLogin = true
return Promise.resolve() // 登录成功
} catch (error) {
console.error('登录失败:', error)
return Promise.reject(error) // 登录失败,抛出错误
}
},
// 异步获取用户信息
async fetchUserInfo() {
try {
const { data } = await getUserInfoApi() // 调用获取用户信息API
this.userInfo = data
localStorage.setItem('userInfo', JSON.stringify(data))
} catch (error) {
console.error('获取用户信息失败:', error)
// 信息获取失败,清除token并退出登录
this.logout()
}
},
// 异步退出登录
async logout() {
try {
await logoutApi() // 调用退出登录API(通知后端销毁token)
} finally {
// 无论API是否成功,都清除本地状态
this.token = ''
this.userInfo = {}
this.isLogin = false
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
}
}
})
// 2. 购物车Store(src/store/modules/cartStore.js)
import { defineStore } from 'pinia'
import { getCartListApi, addCartApi, removeCartApi, updateCartApi } from '@/api/cart'
export const useCartStore = defineStore('cart', {
state: () => ({
cartList: [], // 购物车列表
totalCount: 0, // 购物车商品总数
totalPrice: 0 // 购物车商品总价
}),
getters: {
// 计算是否全选(所有商品都选中则返回true)
isAllSelected: (state) => {
if (state.cartList.length === 0) return false
return state.cartList.every(item => item.selected)
},
// 计算选中商品的数量
selectedCount: (state) => {
return state.cartList.filter(item => item.selected).reduce((sum, item) => sum + item.quantity, 0)
},
// 计算选中商品的总价
selectedPrice: (state) => {
return state.cartList.filter(item => item.selected).reduce((sum, item) => sum + (item.price * item.quantity), 0).toFixed(2)
}
},
actions: {
// 初始化购物车:获取购物车列表
async initCart() {
try {
const { data } = await getCartListApi()
this.cartList = data.list || []
// 更新购物车总数和总价
this.calcTotal()
} catch (error) {
console.error('获取购物车列表失败:', error)
}
},
// 计算购物车总数和总价
calcTotal() {
this.totalCount = this.cartList.reduce((sum, item) => sum + item.quantity, 0)
this.totalPrice = this.cartList.reduce((sum, item) => sum + (item.price * item.quantity), 0).toFixed(2)
},
// 新增商品到购物车
async addToCart(productData) {
try {
await addCartApi(productData) // 调用添加购物车API
await this.initCart() // 重新获取购物车列表,更新状态
return Promise.resolve('添加成功')
} catch (error) {
console.error('添加购物车失败:', error)
return Promise.reject(error)
}
},
// 移除购物车商品
async removeFromCart(cartId) {
try {
await removeCartApi(cartId) // 调用删除购物车API
await this.initCart() // 重新获取购物车列表
} catch (error) {
console.error('删除购物车商品失败:', error)
}
},
// 更新购物车商品(数量/选中状态)
async updateCart(cartId, updateData) {
try {
await updateCartApi(cartId, updateData) // 调用更新购物车API
// 本地临时更新状态(优化体验,避免等待API响应)
const index = this.cartList.findIndex(item => item.id === cartId)
if (index !== -1) {
this.cartList[index] = { ...this.cartList[index], ...updateData }
this.calcTotal() // 重新计算总数和总价
}
} catch (error) {
console.error('更新购物车商品失败:', error)
await this.initCart() // 失败后重新获取,确保状态一致
}
},
// 全选/取消全选
async toggleAllSelect(isSelected) {
try {
// 批量更新所有商品的选中状态
const updatePromises = this.cartList.map(item =>
updateCartApi(item.id, { selected: isSelected })
)
await Promise.all(updatePromises) // 等待所有更新完成
// 本地更新状态
this.cartList.forEach(item => item.selected = isSelected)
this.calcTotal()
} catch (error) {
console.error('全选操作失败:', error)
await this.initCart()
}
}
}
})
// 3. 统一导出Store(src/store/index.js)
export * from './modules/userStore'
export * from './modules/cartStore'(2)Pinia 状态持久化(避免刷新丢失)
虽然可通过localStorage手动存储状态,但使用pinia-plugin-persistedstate插件能更优雅地实现持久化,支持指定字段、存储介质(localStorage/sessionStorage)。
# 安装持久化插件
npm install pinia-plugin-persistedstate
// 1. 配置Pinia与持久化插件(src/main.js)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入插件
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
// 安装持久化插件
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.mount('#app')// 2. 在Store中配置持久化(以用户Store为例)
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: {},
isLogin: false
}),
// 新增persist配置:实现状态持久化
persist: {
key: 'user-store', // 存储的键名(默认是store的id)
storage: localStorage, // 存储介质:localStorage(默认)或sessionStorage
paths: ['token', 'userInfo'] // 指定需要持久化的字段(默认所有字段)
// 排除字段:可通过不列入paths实现,如不持久化isLogin
},
// ... 其余getters、actions不变
})
// 购物车Store同理配置持久化
export const useCartStore = defineStore('cart', {
state: () => ({
cartList: [],
totalCount: 0,
totalPrice: 0
}),
persist: {
key: 'cart-store',
storage: localStorage,
paths: ['cartList'] // 仅持久化购物车列表,总数和总价可通过计算获取
},
// ... 其余getters、actions不变
})(3)组件中使用多模块 Store
<template>
<div class="cart-page">
<h3>我的购物车</h3>
<!-- 未登录提示 -->
<div v-if="!userStore.isLogin" class="login-tip">
<p>请先<a @click="goToLogin">登录</a>查看购物车</p>
</div>
<!-- 购物车列表(已登录) -->
<div v-else class="cart-container">
<!-- 全选与统计 -->
<div class="cart-header">
<label>
<input
type="checkbox"
v-model="cartStore.isAllSelected"
@change="cartStore.toggleAllSelect($event.target.checked)"
>
全选
</label>
<div class="cart-stats">
<p>已选商品:{{ cartStore.selectedCount }}件</p>
<p>总价:¥{{ cartStore.selectedPrice }}</p>
<button class="checkout-btn" :disabled="cartStore.selectedCount === 0">
去结算
</button>
</div>
</div>
<!-- 购物车商品列表 -->
<div class="cart-list">
<div class="cart-item" v-for="item in cartStore.cartList" :key="item.id">
<label>
<input
type="checkbox"
v-model="item.selected"
@change="cartStore.updateCart(item.id, { selected: item.selected })"
>
</label>
<img :src="item.imgUrl" alt="商品图片" class="item-img">
<div class="item-info">
<p class="item-name">{{ item.name }}</p>
<p class="item-price">¥{{ item.price }}</p>
</div>
<div class="item-quantity">
<button @click="cartStore.updateCart(item.id, { quantity: item.quantity - 1 })" :disabled="item.quantity <= 1">
-
</button>
<span>{{ item.quantity }}</span>
<button @click="cartStore.updateCart(item.id, { quantity: item.quantity + 1 })">
+
</button>
</div>
<button class="item-remove" @click="cartStore.removeFromCart(item.id)">
删除
</button>
</div>
</div>
<!-- 空购物车提示 -->
<div v-if="cartStore.cartList.length === 0" class="empty-cart">
<p>购物车是空的,去<a @click="goToHome">首页</a>添加商品吧~</p>
</div>
</div>
</div>
</template>
<script setup>
import { useUserStore } from '@/store/modules/userStore'
import { useCartStore } from '@/store/modules/cartStore'
import { useRouter } from 'vue-router'
import { onMounted } from 'vue'
// 获取Store实例
const userStore = useUserStore()
const cartStore = useCartStore()
const router = useRouter()
// 组件挂载时初始化购物车(仅已登录)
onMounted(() => {
if (userStore.isLogin) {
cartStore.initCart()
}
})
// 跳转登录页
const goToLogin = () => {
router.push('/login')
}
// 跳转首页
const goToHome = () => {
router.push('/')
}
</script>
<style scoped>
.cart-page {
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
}
.login-tip {
padding: 50px;
text-align: center;
color: #666;
}
.login-tip a {
color: #42b983;
cursor: pointer;
text-decoration: underline;
}
.cart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #eee;
}
.cart-stats {
display: flex;
gap: 20px;
align-items: center;
}
.checkout-btn {
padding: 8px 20px;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.checkout-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.cart-list {
margin-top: 20px;
}
.cart-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
gap: 20px;
}
.item-img {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.item-info {
flex: 1;
}
.item-name {
margin: 0 0 10px;
color: #333;
}
.item-price {
margin: 0;
color: #ff4444;
}
.item-quantity {
display: flex;
align-items: center;
gap: 10px;
}
.item-quantity button {
width: 30px;
height: 30px;
border: 1px solid #eee;
background: white;
cursor: pointer;
border-radius: 4px;
}
.item-quantity button:disabled {
color: #ccc;
cursor: not-allowed;
}
.item-remove {
color: #666;
border: none;
background: transparent;
cursor: pointer;
}
.item-remove:hover {
color: #ff4444;
}
.empty-cart {
padding: 50px;
text-align: center;
color: #666;
}
.empty-cart a {
color: #42b983;
cursor: pointer;
text-decoration: underline;
}
</style>
