背景:在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: stringgender: string
}interface User {id: numberusername: stringgender: stringage: numberemail: stringphone: 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) : trueconst genderMatch = filterForm.gender ? user.gender === filterForm.gender : truereturn usernameMatch && genderMatch})
}const resetFilter = () => {filterForm.username = ''filterForm.gender = ''filteredUsers.value = users.valuerouter.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: numberusername: stringgender: stringage: numberemail: stringphone: 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: numberusername: stringgender: stringage: numberemail: stringphone: stringdepartment: stringposition: stringcreateTime: string
}export interface UserFilter {username: stringgender: stringdepartment: 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) : trueconst genderMatch = appliedFilter.value.gender ? user.gender === appliedFilter.value.gender : trueconst departmentMatch = appliedFilter.value.department ? user.department === appliedFilter.value.department : truereturn 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 = truetry {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)) + 1users.value.push({...userData,id: newId})}return {// Stateusers,filterForm,appliedFilter,cachedFilterForm,loading,currentUser,// GettersfilteredUsers,departments,hasCachedFilter,hasAppliedFilter,// ActionssetFilter,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="請輸入用戶名" clearablestyle="width: 200px"@keyup.enter="handleFilter"/></el-form-item><el-form-item label="性別"><el-select v-model="userStore.filterForm.gender" placeholder="請選擇性別" clearablestyle="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="請選擇部門" clearablestyle="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"stripeborder><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>
優點: 無需手動管理數據,組件狀態自動保留。
缺點: 緩存所有組件可能導致內存占用過高。
根據項目規模合數據復雜度選擇合適的方案,也可組合使用多種方式。