背景:在Vue项目中,实现列表页跳转详情页并保留表单数据,返回时恢复表单状态。
核心功能:
- 保存缓存:点击查询按钮时,表单数据保存
- 恢复缓存:从详情页返回时,恢复表单数据
- 清除缓存:页面刷新时自动清除缓存数据
实现以上功能,常见有以下3种方式:
- 路由参数/查询字符串
将表单数据通过路由参数传递,详情页返回时带回。
代码如下:
userList.vue
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElTable, ElTableColumn, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElButton } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'
interface UserFilter {
username: string
gender: string
}
interface User {
id: number
username: string
gender: string
age: number
email: string
phone: string
}
const route = useRoute()
const router = useRouter()
const filterForm = reactive<UserFilter>({
username: '',
gender: ''
})
const users = ref<User[]>([
{
id: 1,
username: '张三',
gender: '男',
age: 25,
email: 'zhangsan@example.com',
phone: '13800138000'
},
{
id: 2,
username: '李四',
gender: '女',
age: 28,
email: 'lisi@example.com',
phone: '13800138001'
},
])
const filteredUsers = ref<User[]>(users.value)
onMounted(() => {
if (route.query.username) {
filterForm.username = route.query.username as string
}
if (route.query.gender) {
filterForm.gender = route.query.gender as string
}
if (filterForm.username || filterForm.gender) {
handleFilter()
}
})
const handleFilter = () => {
filteredUsers.value = users.value.filter(user => {
const usernameMatch = filterForm.username ? user.username.includes(filterForm.username) : true
const genderMatch = filterForm.gender ? user.gender === filterForm.gender : true
return usernameMatch && genderMatch
})
}
const resetFilter = () => {
filterForm.username = ''
filterForm.gender = ''
filteredUsers.value = users.value
router.replace({ query: {} })
}
const viewDetail = (row: User) => {
router.push({
name: 'userDetail',
params: { id: row.id },
query: {
username: filterForm.username,
gender: filterForm.gender
}
})
}
</script>
<template>
<div class="user-list">
<el-form :model="filterForm" inline class="filter-form">
<el-form-item label="用户名">
<el-input v-model="filterForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="filterForm.gender" placeholder="请选择性别" style="width: 200px">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFilter">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="filteredUsers" style="width: 100%">
<el-table-column prop="username" label="用户名" />
<el-table-column prop="gender" label="性别" />
<el-table-column prop="age" label="年龄" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="phone" label="电话" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" size="small" @click="viewDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<style scoped>
.user-list {
padding: 20px;
}
.filter-form {
margin-bottom: 20px;
}
</style>
userDetail.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElButton, ElDescriptions, ElDescriptionsItem } from 'element-plus'
const route = useRoute()
const router = useRouter()
interface User {
id: number
username: string
gender: string
age: number
email: string
phone: string
}
const user = ref<User>()
onMounted(() => {
// 实际开发中,调用接口获取数据
user.value = {
id: 1,
username: '张三',
gender: '男',
age: 25,
email: 'zhangsan@example.com',
phone: '13800138000'
}
})
const goBack = () => {
router.push({
name: 'userList',
query: {
username: route.query.username,
gender: route.query.gender
}
})
}
</script>
<template>
<div class="user-detail">
<div class="header">
<h2>用户详情</h2>
<el-button @click="goBack">返回</el-button>
</div>
<el-descriptions v-if="user" :column="2" border>
<el-descriptions-item label="用户名">{{ user.username }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ user.gender }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ user.age }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ user.email }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ user.phone }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<style scoped>
.user-detail {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
</style>
**优点:**简单直接,无需额外存储。
**缺点:**数量有限(URL长度限制),敏感数据不安全。
效果如图所示:
2. Vuex/Pinia状态管理
将表单数据存储在全局状态中,详情页返回时从状态中恢复。
代码实现如下:
建立一个stores文件夹,建user.ts文件
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface User {
id: number
username: string
gender: string
age: number
email: string
phone: string
department: string
position: string
createTime: string
}
export interface UserFilter {
username: string
gender: string
department: string
}
export const useUserStore = defineStore('user', () => {
const users = ref<User[]>([
{
id: 1,
username: '张三',
gender: '男',
age: 25,
email: 'zhangsan@example.com',
phone: '13800138000',
department: '技术部',
position: '前端工程师',
createTime: '2023-01-15'
},
{
id: 2,
username: '李四',
gender: '女',
age: 28,
email: 'lisi@example.com',
phone: '13800138001',
department: '产品部',
position: '产品经理',
createTime: '2023-02-20'
},
{
id: 3,
username: '王五',
gender: '男',
age: 32,
email: 'wangwu@example.com',
phone: '13800138002',
department: '技术部',
position: '后端工程师',
createTime: '2023-03-10'
},
{
id: 4,
username: '赵六',
gender: '女',
age: 26,
email: 'zhaoliu@example.com',
phone: '13800138003',
department: '设计部',
position: 'UI设计师',
createTime: '2023-04-05'
},
{
id: 5,
username: '钱七',
gender: '男',
age: 30,
email: 'qianqi@example.com',
phone: '13800138004',
department: '运营部',
position: '运营专员',
createTime: '2023-05-12'
},
{
id: 6,
username: '孙八',
gender: '女',
age: 24,
email: 'sunba@example.com',
phone: '13800138005',
department: '技术部',
position: '测试工程师',
createTime: '2023-06-18'
},
{
id: 7,
username: '周九',
gender: '男',
age: 29,
email: 'zhoujiu@example.com',
phone: '13800138006',
department: '产品部',
position: '产品助理',
createTime: '2023-07-22'
},
{
id: 8,
username: '吴十',
gender: '女',
age: 27,
email: 'wushi@example.com',
phone: '13800138007',
department: '设计部',
position: '视觉设计师',
createTime: '2023-08-14'
}
])
// 表单数据(用于输入)
const filterForm = ref<UserFilter>({
username: '',
gender: '',
department: ''
})
// 实际应用的筛选条件(只在点击查询时更新)
const appliedFilter = ref<UserFilter>({
username: '',
gender: '',
department: ''
})
// 缓存的表单数据
const cachedFilterForm = ref<UserFilter>({
username: '',
gender: '',
department: ''
})
const loading = ref(false)
const currentUser = ref<User | null>(null)
// Getters - 基于 appliedFilter 进行筛选
const filteredUsers = computed(() => {
return users.value.filter(user => {
const usernameMatch = appliedFilter.value.username
? user.username.includes(appliedFilter.value.username)
: true
const genderMatch = appliedFilter.value.gender
? user.gender === appliedFilter.value.gender
: true
const departmentMatch = appliedFilter.value.department
? user.department === appliedFilter.value.department
: true
return usernameMatch && genderMatch && departmentMatch
})
})
const departments = computed(() => {
const depts = [...new Set(users.value.map(user => user.department))]
return depts.sort()
})
const setFilter = (filter: Partial<UserFilter>) => {
Object.assign(filterForm.value, filter)
}
// 应用筛选条件(点击查询按钮时调用)
const applyFilter = () => {
appliedFilter.value = { ...filterForm.value }
}
const resetFilter = () => {
filterForm.value = {
username: '',
gender: '',
department: ''
}
appliedFilter.value = {
username: '',
gender: '',
department: ''
}
}
// 保存表单数据到缓存
const saveFilterToCache = () => {
cachedFilterForm.value = { ...filterForm.value }
}
// 从缓存恢复表单数据
const restoreFilterFromCache = () => {
filterForm.value = { ...cachedFilterForm.value }
// 同时应用筛选条件
appliedFilter.value = { ...cachedFilterForm.value }
}
// 清除缓存的表单数据
const clearFilterCache = () => {
cachedFilterForm.value = {
username: '',
gender: '',
department: ''
}
}
// 检查是否有缓存的表单数据
const hasCachedFilter = computed(() => {
return Object.values(cachedFilterForm.value).some(value => value !== '')
})
// 检查是否有应用的筛选条件
const hasAppliedFilter = computed(() => {
return Object.values(appliedFilter.value).some(value => value !== '')
})
const getUserById = (id: number): User | undefined => {
return users.value.find(user => user.id === id)
}
const setCurrentUser = (user: User | null) => {
currentUser.value = user
}
const fetchUserById = async (id: number): Promise<User | null> => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const user = getUserById(id)
setCurrentUser(user || null)
return user || null
} finally {
loading.value = false
}
}
const updateUser = (id: number, userData: Partial<User>) => {
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value[index] = { ...users.value[index], ...userData }
}
}
const deleteUser = (id: number) => {
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value.splice(index, 1)
}
}
const addUser = (userData: Omit<User, 'id'>) => {
const newId = Math.max(...users.value.map(u => u.id)) + 1
users.value.push({
...userData,
id: newId
})
}
return {
// State
users,
filterForm,
appliedFilter,
cachedFilterForm,
loading,
currentUser,
// Getters
filteredUsers,
departments,
hasCachedFilter,
hasAppliedFilter,
// Actions
setFilter,
applyFilter,
resetFilter,
saveFilterToCache,
restoreFilterFromCache,
clearFilterCache,
getUserById,
setCurrentUser,
fetchUserById,
updateUser,
deleteUser,
addUser
}
}, {
persist: {
key: 'user-store',
storage: localStorage,
paths: ['cachedFilterForm'] // 只持久化缓存的表单数据
}
})
userList.vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import {
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElButton,
ElTag,
ElLoading,
ElMessage
} from 'element-plus'
import { useRouter } from 'vue-router'
import { useUserStore, type User } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
// 页面初始化时从缓存恢复表单数据
onMounted(() => {
if (userStore.hasCachedFilter) {
userStore.restoreFilterFromCache()
}
})
// 页面刷新时清除缓存
onBeforeUnmount(() => {
userStore.clearFilterCache()
})
// 监听页面刷新事件,清除缓存的表单数据
window.addEventListener('beforeunload', () => {
userStore.clearFilterCache()
})
const handleFilter = () => {
// 应用筛选条件
userStore.applyFilter()
// 保存到缓存
userStore.saveFilterToCache()
}
const resetFilter = () => {
userStore.resetFilter()
userStore.clearFilterCache()
}
const viewDetail = (row: User) => {
// 查看详情前保存当前筛选状态
userStore.saveFilterToCache()
router.push({
name: 'userDetail',
params: { id: row.id.toString() }
})
}
</script>
<template>
<div class="user-list">
<div class="header">
<h2>用户管理</h2>
</div>
<el-form :model="userStore.filterForm" inline class="filter-form">
<el-form-item label="用户名">
<el-input
v-model="userStore.filterForm.username"
placeholder="请输入用户名"
clearable
style="width: 200px"
@keyup.enter="handleFilter"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
v-model="userStore.filterForm.gender"
placeholder="请选择性别"
clearable
style="width: 120px"
>
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="部门">
<el-select
v-model="userStore.filterForm.department"
placeholder="请选择部门"
clearable
style="width: 150px"
>
<el-option
v-for="dept in userStore.departments"
:key="dept"
:label="dept"
:value="dept"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFilter">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
<!-- User Table -->
<el-table
:data="userStore.filteredUsers"
style="width: 100%"
v-loading="userStore.loading"
stripe
border
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="gender" label="性别" width="80" />
<el-table-column prop="age" label="年龄" width="80" />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职位" width="150" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="phone" label="电话" width="130" />
<el-table-column prop="createTime" label="创建时间" width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="viewDetail(row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="userStore.filteredUsers.length === 0 && !userStore.loading" class="empty-state">
<p>{{ userStore.hasAppliedFilter ? '没有找到符合条件的用户' : '暂无用户数据' }}</p>
</div>
</div>
</template>
<style scoped>
.user-list {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #303133;
}
.header-info {
display: flex;
align-items: center;
}
.cache-indicator {
color: #67c23a;
font-size: 14px;
background: #f0f9ff;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #b3d8ff;
}
.filter-form {
margin-bottom: 20px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.empty-state {
text-align: center;
padding: 60px 0;
color: #909399;
}
.empty-state p {
font-size: 16px;
margin: 0;
}
</style>
userDetail.vue
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ElButton,
ElDescriptions,
ElDescriptionsItem,
ElTag,
ElCard,
ElLoading,
ElMessage
} from 'element-plus'
import { useUserStore } from '../stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const userId = computed(() => parseInt(route.params.id as string))
onMounted(async () => {
if (userId.value) {
await userStore.fetchUserById(userId.value)
}
})
const goBack = () => {
router.push({
name: 'userList'
})
}
</script>
<template>
<div class="user-detail" v-loading="userStore.loading">
<div class="header">
<h2>用户详情</h2>
<div class="header-actions">
<el-button @click="goBack" type="primary">返回列表</el-button>
</div>
</div>
<el-card v-if="userStore.currentUser" class="detail-card">
<template #header>
<div class="card-header">
<span class="user-name">{{ userStore.currentUser.username }}</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="用户ID">
{{ userStore.currentUser.id }}
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ userStore.currentUser.username }}
</el-descriptions-item>
<el-descriptions-item label="性别">
{{ userStore.currentUser.gender }}
</el-descriptions-item>
<el-descriptions-item label="年龄">
{{ userStore.currentUser.age }} 岁
</el-descriptions-item>
<el-descriptions-item label="部门">
{{ userStore.currentUser.department }}
</el-descriptions-item>
<el-descriptions-item label="职位">
{{ userStore.currentUser.position }}
</el-descriptions-item>
<el-descriptions-item label="邮箱" :span="2">
{{ userStore.currentUser.email }}
</el-descriptions-item>
<el-descriptions-item label="电话">
{{ userStore.currentUser.phone }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ userStore.currentUser.createTime }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<div v-else-if="!userStore.loading" class="no-data">
<p>用户不存在</p>
<el-button @click="goBack" type="primary">返回列表</el-button>
</div>
</div>
</template>
<style scoped>
.user-detail {
padding: 20px;
min-height: 400px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.cache-info {
color: #67c23a;
font-size: 14px;
background: #f0f9ff;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #b3d8ff;
}
.detail-card {
max-width: 800px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.user-name {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.no-data {
text-align: center;
padding: 60px 0;
color: #909399;
}
.no-data p {
font-size: 16px;
margin-bottom: 20px;
}
</style>
使用 pinia-plugin-persistedstate 插件实现 localStorage 持久化
Pinia持久化插件详细信息
效果如图所示:
优点: 数据全局共享,适合复杂场景。
缺点: 需额外维护状态,可能导致store臃肿。
3. keep-alive缓存组件
使用包裹列表组件,被缓存的列表组件在切换路由时不会被销毁,而是进入“休眠”状态,其数据和DOM结构会被保留,返回值保留状态。
App.vue
<router-view v-slot="{ Component }">
<keep-alive include="List">
<component :is="Component" />
</keep-alive>
</router-view>
优点: 无需手动管理数据,组件状态自动保留。
缺点: 缓存所有组件可能导致内存占用过高。
根据项目规模合数据复杂度选择合适的方案,也可组合使用多种方式。