Vue 项目实战:三种方式实现列表→详情页表单数据保留与恢复

发布于:2025-06-11 ⋅ 阅读:(17) ⋅ 点赞:(0)

背景:在Vue项目中,实现列表页跳转详情页并保留表单数据,返回时恢复表单状态。
核心功能

  1. 保存缓存:点击查询按钮时,表单数据保存
  2. 恢复缓存:从详情页返回时,恢复表单数据
  3. 清除缓存:页面刷新时自动清除缓存数据

实现以上功能,常见有以下3种方式:

  1. 路由参数/查询字符串
    将表单数据通过路由参数传递,详情页返回时带回。
    代码如下:

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>

优点: 无需手动管理数据,组件状态自动保留。
缺点: 缓存所有组件可能导致内存占用过高。
根据项目规模合数据复杂度选择合适的方案,也可组合使用多种方式。