history保存列表页ajax请求的状态使用示例详解
2026-04-01 18:06:03
浏览次数:1
Ajax状态保存示例详解
下面是一个完整的Ajax状态保存列表页实现示例,包括完整的HTML、CSS和JavaScript代码。
- index.html # 主页面
- ajax-handler.php # 模拟API接口
- README.md # 说明文档
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ajax状态保存列表页示例</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f7fa;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
header {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: white;
padding: 25px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2.2rem;
margin-bottom: 10px;
}
.subtitle {
opacity: 0.9;
font-size: 1.1rem;
}
.container {
display: grid;
grid-template-columns: 1fr 300px;
gap: 30px;
}
.main-content {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
}
.sidebar {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
}
h2 {
color: #2c3e50;
padding-bottom: 15px;
margin-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-success {
background: #2ecc71;
color: white;
}
.btn-success:hover {
background: #27ae60;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
select, input {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
}
.loading-indicator {
display: none;
text-align: center;
padding: 20px;
color: #3498db;
font-weight: bold;
}
.loading-indicator.active {
display: block;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.status-indicator {
padding: 8px 15px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
display: inline-block;
margin-bottom: 15px;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.data-list {
list-style: none;
margin-top: 20px;
}
.data-item {
padding: 15px;
margin-bottom: 10px;
background: #f8f9fa;
border-left: 4px solid #3498db;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s ease;
}
.data-item:hover {
transform: translateX(5px);
background: #eef5ff;
}
.item-title {
font-weight: 600;
color: #2c3e50;
}
.item-meta {
font-size: 0.9rem;
color: #7f8c8d;
}
.state-display {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
}
.tab-container {
margin-bottom: 25px;
}
.tabs {
display: flex;
border-bottom: 2px solid #f0f0f0;
margin-bottom: 20px;
}
.tab {
padding: 12px 25px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.tab.active {
border-bottom-color: #3498db;
color: #3498db;
font-weight: 600;
}
.tab:hover:not(.active) {
background: #f8f9fa;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.storage-info {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #eee;
}
.info-label {
font-weight: 600;
}
.info-value {
color: #3498db;
}
footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: #7f8c8d;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
}
</style>
</head>
<body>
<header>
<h1>Ajax状态保存列表页示例</h1>
<p class="subtitle">演示如何保存和恢复Ajax请求的状态,提升用户体验</p>
</header>
<div class="container">
<main class="main-content">
<div class="tab-container">
<div class="tabs">
<div class="tab active" data-tab="data">数据列表</div>
<div class="tab" data-tab="state">状态信息</div>
<div class="tab" data-tab="settings">设置</div>
</div>
<div id="data-tab" class="tab-content active">
<h2>数据列表</h2>
<div class="status-indicator status-pending" id="status-indicator">
状态:未加载
</div>
<div class="controls">
<button class="btn btn-primary" id="load-data">加载数据</button>
<button class="btn btn-success" id="load-more">加载更多</button>
<select id="filter-category">
<option value="">所有分类</option>
<option value="tech">科技</option>
<option value="sports">体育</option>
<option value="finance">财经</option>
<option value="entertainment">娱乐</option>
</select>
<select id="sort-order">
<option value="asc">升序</option>
<option value="desc">降序</option>
</select>
<input type="number" id="page-size" placeholder="每页数量" value="5" min="1" max="20">
</div>
<div class="loading-indicator" id="loading">
正在加载数据,请稍候...
</div>
<ul class="data-list" id="data-list">
<!-- 数据将在这里动态加载 -->
</ul>
<div class="pagination-info" id="pagination-info">
第 <span id="current-page">1</span> 页,共 <span id="total-pages">1</span> 页,显示 <span id="item-count">0</span> 条数据
</div>
</div>
<div id="state-tab" class="tab-content">
<h2>当前状态信息</h2>
<p>当前保存的Ajax请求状态信息:</p>
<div class="state-display" id="state-display">
// 状态信息将在这里显示
</div>
<div class="controls">
<button class="btn btn-secondary" id="refresh-state">刷新状态</button>
<button class="btn btn-warning" id="clear-state">清除状态</button>
</div>
</div>
<div id="settings-tab" class="tab-content">
<h2>状态保存设置</h2>
<div class="storage-info">
<div class="info-item">
<span class="info-label">存储方式:</span>
<span class="info-value" id="storage-type">localStorage</span>
</div>
<div class="info-item">
<span class="info-label">状态保存:</span>
<span class="info-value">
<label>
<input type="checkbox" id="auto-save" checked> 自动保存状态
</label>
</span>
</div>
<div class="info-item">
<span class="info-label">恢复上次状态:</span>
<span class="info-value">
<label>
<input type="checkbox" id="auto-restore" checked> 页面加载时自动恢复
</label>
</span>
</div>
<div class="info-item">
<span class="info-label">状态保存时间:</span>
<span class="info-value" id="last-save-time">从未保存</span>
</div>
<div class="info-item">
<span class="info-label">状态有效期:</span>
<span class="info-value">
<select id="state-expiry">
<option value="1">1小时</option>
<option value="6" selected>6小时</option>
<option value="24">24小时</option>
<option value="168">7天</option>
<option value="0">永久</option>
</select>
</span>
</div>
</div>
</div>
</div>
</main>
<aside class="sidebar">
<h2>状态保存说明</h2>
<p><strong>实现原理:</strong></p>
<ul>
<li>使用localStorage保存Ajax请求状态</li>
<li>保存参数:页码、分类、排序、每页数量</li>
<li>保存时间戳,支持过期机制</li>
<li>页面加载时自动恢复上次状态</li>
</ul>
<p><strong>操作指南:</strong></p>
<ol>
<li>点击"加载数据"从服务器获取数据</li>
<li>更改筛选条件会自动保存状态</li>
<li>刷新页面或重新打开时会自动恢复</li>
<li>可在"状态信息"标签查看当前状态</li>
</ol>
<p><strong>适用场景:</strong></p>
<ul>
<li>商品列表页筛选</li>
<li>新闻/文章列表</li>
<li>数据仪表盘</li>
<li>任何需要保持用户操作状态的列表页</li>
</ul>
<div class="state-display" id="current-state-summary">
// 状态摘要
</div>
</aside>
</div>
<footer>
<p>Ajax状态保存示例 © 2023 | 演示如何提升用户体验</p>
</footer>
<script>
// 状态管理对象
const StateManager = {
// 状态存储键名
STORAGE_KEY: 'ajax_list_state',
// 当前状态
currentState: {
page: 1,
category: '',
sortOrder: 'asc',
pageSize: 5,
lastUpdated: null,
totalItems: 0,
totalPages: 1
},
// 初始化状态
init() {
// 尝试从localStorage恢复状态
this.restoreState();
// 绑定UI事件
this.bindEvents();
// 更新状态显示
this.updateDisplay();
// 如果设置了自动恢复,则加载数据
const autoRestore = document.getElementById('auto-restore').checked;
if (autoRestore && this.currentState.lastUpdated) {
// 检查状态是否过期
if (!this.isStateExpired()) {
this.loadDataFromState();
}
}
},
// 保存状态到localStorage
saveState() {
// 更新最后保存时间
this.currentState.lastUpdated = new Date().toISOString();
// 保存到localStorage
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.currentState));
// 更新状态显示
this.updateDisplay();
console.log('状态已保存:', this.currentState);
},
// 从localStorage恢复状态
restoreState() {
try {
const savedState = localStorage.getItem(this.STORAGE_KEY);
if (savedState) {
const parsedState = JSON.parse(savedState);
// 检查状态是否过期
if (this.isStateExpired(parsedState)) {
console.log('保存的状态已过期,使用默认状态');
return;
}
// 恢复状态
this.currentState = { ...this.currentState, ...parsedState };
// 更新UI控件
document.getElementById('filter-category').value = this.currentState.category || '';
document.getElementById('sort-order').value = this.currentState.sortOrder || 'asc';
document.getElementById('page-size').value = this.currentState.pageSize || 5;
document.getElementById('current-page').textContent = this.currentState.page || 1;
console.log('状态已恢复:', this.currentState);
}
} catch (error) {
console.error('恢复状态时出错:', error);
}
},
// 检查状态是否过期
isStateExpired(state = this.currentState) {
if (!state.lastUpdated) return true;
const expiryHours = parseInt(document.getElementById('state-expiry').value);
if (expiryHours === 0) return false; // 0表示永不过期
const lastUpdated = new Date(state.lastUpdated);
const now = new Date();
const hoursDiff = (now - lastUpdated) / (1000 * 60 * 60);
return hoursDiff > expiryHours;
},
// 更新状态
updateState(updates) {
this.currentState = { ...this.currentState, ...updates };
// 自动保存设置
const autoSave = document.getElementById('auto-save').checked;
if (autoSave) {
this.saveState();
}
this.updateDisplay();
},
// 清除状态
clearState() {
localStorage.removeItem(this.STORAGE_KEY);
this.currentState = {
page: 1,
category: '',
sortOrder: 'asc',
pageSize: 5,
lastUpdated: null,
totalItems: 0,
totalPages: 1
};
// 重置UI
document.getElementById('filter-category').value = '';
document.getElementById('sort-order').value = 'asc';
document.getElementById('page-size').value = 5;
document.getElementById('current-page').textContent = 1;
document.getElementById('total-pages').textContent = 1;
document.getElementById('item-count').textContent = 0;
// 清空列表
document.getElementById('data-list').innerHTML = '';
// 更新状态显示
this.updateDisplay();
alert('状态已清除');
},
// 绑定事件
bindEvents() {
// 加载数据按钮
document.getElementById('load-data').addEventListener('click', () => {
this.currentState.page = 1;
this.loadData();
});
// 加载更多按钮
document.getElementById('load-more').addEventListener('click', () => {
this.currentState.page += 1;
this.loadData(true);
});
// 筛选条件变化
document.getElementById('filter-category').addEventListener('change', (e) => {
this.updateState({ category: e.target.value, page: 1 });
this.loadData();
});
document.getElementById('sort-order').addEventListener('change', (e) => {
this.updateState({ sortOrder: e.target.value, page: 1 });
this.loadData();
});
document.getElementById('page-size').addEventListener('change', (e) => {
this.updateState({ pageSize: parseInt(e.target.value), page: 1 });
this.loadData();
});
// 状态管理按钮
document.getElementById('refresh-state').addEventListener('click', () => {
this.updateDisplay();
});
document.getElementById('clear-state').addEventListener('click', () => {
this.clearState();
});
// 标签切换
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabId = e.target.getAttribute('data-tab');
this.switchTab(tabId);
});
});
// 设置变化
document.getElementById('auto-save').addEventListener('change', () => {
this.updateDisplay();
});
document.getElementById('auto-restore').addEventListener('change', () => {
this.updateDisplay();
});
document.getElementById('state-expiry').addEventListener('change', () => {
this.updateDisplay();
});
},
// 切换标签页
switchTab(tabId) {
// 更新标签激活状态
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelector(`.tab[data-tab="${tabId}"]`).classList.add('active');
// 更新内容显示
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabId}-tab`).classList.add('active');
// 如果切换到状态标签,刷新显示
if (tabId === 'state') {
this.updateDisplay();
}
},
// 加载数据
async loadData(append = false) {
const loadingIndicator = document.getElementById('loading');
const statusIndicator = document.getElementById('status-indicator');
const dataList = document.getElementById('data-list');
// 显示加载状态
loadingIndicator.classList.add('active');
statusIndicator.textContent = '状态:加载中...';
statusIndicator.className = 'status-indicator status-pending';
try {
// 构建请求参数
const params = {
page: this.currentState.page,
category: this.currentState.category,
sort: this.currentState.sortOrder,
pageSize: this.currentState.pageSize
};
// 模拟API请求
const data = await this.mockApiRequest(params);
// 更新状态
this.updateState({
totalItems: data.totalItems,
totalPages: data.totalPages
});
// 更新分页信息
document.getElementById('current-page').textContent = this.currentState.page;
document.getElementById('total-pages').textContent = this.currentState.totalPages;
document.getElementById('item-count').textContent = this.currentState.totalItems;
// 渲染数据
if (!append) {
dataList.innerHTML = '';
}
data.items.forEach(item => {
const li = document.createElement('li');
li.className = 'data-item';
li.innerHTML = `
<div>
<div class="item-title">${item.title}</div>
<div class="item-meta">${item.category} • ${item.date}</div>
</div>
<div>ID: ${item.id}</div>
`;
dataList.appendChild(li);
});
// 更新状态指示器
statusIndicator.textContent = '状态:加载成功';
statusIndicator.className = 'status-indicator status-success';
// 保存状态
this.saveState();
} catch (error) {
console.error('加载数据失败:', error);
// 更新状态指示器
statusIndicator.textContent = `状态:加载失败 - ${error.message}`;
statusIndicator.className = 'status-indicator status-error';
// 恢复页码
if (!append) {
this.currentState.page = 1;
} else {
this.currentState.page -= 1;
}
} finally {
// 隐藏加载指示器
loadingIndicator.classList.remove('active');
}
},
// 从保存的状态加载数据
loadDataFromState() {
this.loadData();
},
// 模拟API请求
mockApiRequest(params) {
return new Promise((resolve, reject) => {
// 模拟网络延迟
setTimeout(() => {
// 模拟随机失败
if (Math.random() < 0.1) { // 10%失败率
reject(new Error('服务器响应超时'));
return;
}
// 模拟数据
const categories = ['tech', 'sports', 'finance', 'entertainment'];
const titles = [
'人工智能助力医疗诊断',
'全球气候变化大会召开',
'数字货币市场波动分析',
'最新智能手机发布',
'国际足球赛事结果',
'电影票房创历史新高',
'科技公司财报公布',
'新能源汽车市场趋势'
];
const items = [];
const startId = (params.page - 1) * params.pageSize + 1;
for (let i = 0; i < params.pageSize; i++) {
const category = params.category || categories[Math.floor(Math.random() * categories.length)];
const titleIndex = Math.floor(Math.random() * titles.length);
items.push({
id: startId + i,
title: titles[titleIndex],
category: category,
date: new Date().toLocaleDateString('zh-CN')
});
}
// 根据筛选条件过滤
let filteredItems = items;
if (params.category) {
filteredItems = items.filter(item => item.category === params.category);
}
// 模拟排序
if (params.sort === 'desc') {
filteredItems.reverse();
}
// 模拟分页数据
const totalItems = params.category ? 35 : 100;
const totalPages = Math.ceil(totalItems / params.pageSize);
resolve({
items: filteredItems,
totalItems,
totalPages,
currentPage: params.page
});
}, 800); // 800ms延迟模拟网络请求
});
},
// 更新状态显示
updateDisplay() {
// 更新状态显示面板
const stateDisplay = document.getElementById('state-display');
stateDisplay.textContent = JSON.stringify(this.currentState, null, 2);
// 更新侧边栏摘要
const summary = document.getElementById('current-state-summary');
const lastSaveTime = this.currentState.lastUpdated
? new Date(this.currentState.lastUpdated).toLocaleString('zh-CN')
: '从未保存';
summary.innerHTML = `
<strong>当前状态摘要:</strong><br>
页码: ${this.currentState.page}<br>
分类: ${this.currentState.category || '全部'}<br>
排序: ${this.currentState.sortOrder === 'asc' ? '升序' : '降序'}<br>
每页: ${this.currentState.pageSize}条<br>
最后保存: ${lastSaveTime}<br>
总数据: ${this.currentState.totalItems}条
`;
// 更新设置面板信息
document.getElementById('last-save-time').textContent =
this.currentState.lastUpdated
? new Date(this.currentState.lastUpdated).toLocaleString('zh-CN')
: '从未保存';
document.getElementById('storage-type').textContent = 'localStorage';
}
};
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
StateManager.init();
});
</script>
</body>
</html>
<?php
// 模拟的API接口
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
// 获取请求参数
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$category = isset($_GET['category']) ? $_GET['category'] : '';
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'asc';
$pageSize = isset($_GET['pageSize']) ? intval($_GET['pageSize']) : 5;
// 模拟数据
$categories = ['tech', 'sports', 'finance', 'entertainment'];
$titles = [
'人工智能助力医疗诊断',
'全球气候变化大会召开',
'数字货币市场波动分析',
'最新智能手机发布',
'国际足球赛事结果',
'电影票房创历史新高',
'科技公司财报公布',
'新能源汽车市场趋势'
];
// 生成数据
$items = [];
$startId = ($page - 1) * $pageSize + 1;
for ($i = 0; $i < $pageSize; $i++) {
$cat = $category ?: $categories[array_rand($categories)];
$title = $titles[array_rand($titles)];
$items[] = [
'id' => $startId + $i,
'title' => $title,
'category' => $cat,
'date' => date('Y-m-d'),
'views' => rand(100, 10000)
];
}
// 筛选数据
if ($category) {
$items = array_filter($items, function($item) use ($category) {
return $item['category'] === $category;
});
$items = array_values($items); // 重新索引数组
}
// 排序
if ($sort === 'desc') {
usort($items, function($a, $b) {
return $b['id'] - $a['id'];
});
}
// 计算总数和总页数
$totalItems = $category ? 35 : 100;
$totalPages = ceil($totalItems / $pageSize);
// 构建响应
$response = [
'success' => true,
'message' => '数据获取成功',
'data' => [
'items' => $items,
'pagination' => [
'currentPage' => $page,
'pageSize' => $pageSize,
'totalItems' => $totalItems,
'totalPages' => $totalPages
],
'filters' => [
'category' => $category,
'sort' => $sort
]
],
'timestamp' => time()
];
// 模拟随机失败
if (rand(1, 10) === 1) { // 10%失败率
http_response_code(500);
$response = [
'success' => false,
'message' => '服务器内部错误,请稍后重试',
'timestamp' => time()
];
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
?>
状态保存:
状态恢复:
用户体验:
状态管理对象:
const StateManager = {
STORAGE_KEY: 'ajax_list_state',
currentState: { /* 状态对象 */ },
saveState() { /* 保存状态 */ },
restoreState() { /* 恢复状态 */ },
updateState() { /* 更新状态 */ }
};
状态保存时机:
状态恢复时机:
支持更多存储方式:
状态压缩:
状态版本管理:
服务端集成:
这个示例提供了一个完整的、可直接运行的Ajax状态保存列表页实现,涵盖了状态管理、UI交互、错误处理和用户体验等各个方面。