编程式导航 | Vue Router 代码控制路由完全指南 🧭
不只是点击链接!用代码精确控制用户的导航体验,让路由跳转更智能、更灵活。
🌟 什么是编程式导航?
编程式导航是指通过 JavaScript 代码来控制路由跳转,而不是依赖用户点击 <router-link> 组件。这让你能够:
- 🎯 条件导航 - 根据业务逻辑决定是否跳转
- 🔄 动态跳转 - 基于用户操作或数据变化触发导航
- 🛡️ 权限控制 - 在跳转前进行权限验证
- 🎮 交互体验 - 结合动画、确认框等增强用户体验
💡 声明式 vs 编程式导航
| 声明式导航 | 编程式导航 |
|---|---|
<router-link :to="..."> | router.push(...) |
| 用户主动点击 | 代码逻辑控制 |
| 静态跳转 | 动态跳转 |
🚀 核心 API 详解
🎯 router.push() - 导航到新位置
router.push() 是最常用的编程式导航方法,它会向浏览器历史栈添加新记录。
📝 基础语法
javascript
// 🎯 获取路由器实例
import { useRouter } from 'vue-router'
const router = useRouter()
// 🚀 各种跳转方式
router.push('/users/john') // 字符串路径
router.push({ path: '/users/john' }) // 路径对象
router.push({ name: 'user', params: { id: 'john' } }) // 命名路由
router.push({ path: '/search', query: { q: 'vue' } }) // 带查询参数
router.push({ path: '/about', hash: '#team' }) // 带锚点1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
🎮 实战示例 - 智能登录跳转
vue
<script setup>
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 🔐 登录状态管理
const isLoggedIn = ref(false)
const userRole = ref('guest')
const loginForm = ref({
username: '',
password: ''
})
// 🎯 智能跳转逻辑
const getRedirectPath = () => {
// 1. 检查是否有重定向参数
const redirect = route.query.redirect
if (redirect) return redirect
// 2. 根据用户角色决定默认页面
const roleRoutes = {
admin: { name: 'admin-dashboard' },
user: { name: 'user-profile' },
guest: { name: 'home' }
}
return roleRoutes[userRole.value] || { name: 'home' }
}
// 🚀 执行登录
async function handleLogin() {
try {
// 🔄 显示加载状态
const loading = showLoading('正在登录...')
// 🌐 调用登录 API
const response = await loginAPI(loginForm.value)
if (response.success) {
// ✅ 登录成功
isLoggedIn.value = true
userRole.value = response.user.role
// 🎉 显示成功消息
showSuccess('登录成功!正在跳转...')
// 🎯 智能跳转
const redirectTo = getRedirectPath()
// ⏰ 延迟跳转,让用户看到成功消息
setTimeout(() => {
router.push(redirectTo)
}, 1000)
} else {
// ❌ 登录失败
showError(response.message || '登录失败,请检查用户名和密码')
}
} catch (error) {
// 🚨 网络错误
showError('网络连接失败,请稍后重试')
} finally {
hideLoading()
}
}
// 🎯 条件导航示例
function navigateToProfile() {
if (!isLoggedIn.value) {
// 🔐 未登录,跳转到登录页并记录当前页面
router.push({
name: 'login',
query: { redirect: route.fullPath }
})
return
}
// ✅ 已登录,直接跳转到个人资料
router.push({ name: 'user-profile' })
}
// 🛒 购物车结算流程
function proceedToCheckout() {
// 1. 检查登录状态
if (!isLoggedIn.value) {
showConfirm({
title: '需要登录',
message: '请先登录后再进行结算',
confirmText: '去登录',
cancelText: '取消',
onConfirm: () => {
router.push({
name: 'login',
query: { redirect: '/checkout' }
})
}
})
return
}
// 2. 检查购物车
if (cartItems.value.length === 0) {
showAlert('购物车为空,请先添加商品')
router.push({ name: 'product-list' })
return
}
// 3. 检查收货地址
if (!hasShippingAddress.value) {
showConfirm({
title: '需要收货地址',
message: '请先设置收货地址',
confirmText: '去设置',
cancelText: '取消',
onConfirm: () => {
router.push({ name: 'address-management' })
}
})
return
}
// ✅ 所有条件满足,进入结算页面
router.push({ name: 'checkout' })
}
</script>
<template>
<div class="login-container">
<!-- 🔐 登录表单 -->
<form @submit.prevent="handleLogin" class="login-form">
<h2>🔐 用户登录</h2>
<div class="form-group">
<label for="username">👤 用户名</label>
<input
id="username"
v-model="loginForm.username"
type="text"
placeholder="请输入用户名"
required
/>
</div>
<div class="form-group">
<label for="password">🔒 密码</label>
<input
id="password"
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
required
/>
</div>
<button type="submit" class="login-btn">
🚀 立即登录
</button>
</form>
<!-- 🎯 导航示例 -->
<div class="navigation-examples">
<h3>🧭 导航示例</h3>
<button @click="navigateToProfile()" class="nav-btn">
👤 查看个人资料
</button>
<button @click="proceedToCheckout()" class="nav-btn">
🛒 立即结算
</button>
<button @click="router.push({ name: 'product-list', query: { category: 'electronics' } })" class="nav-btn">
📱 浏览电子产品
</button>
</div>
</div>
</template>
<style scoped>
.login-container {
max-width: 400px;
margin: 40px auto;
padding: 32px;
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.login-form {
margin-bottom: 32px;
}
.login-form h2 {
text-align: center;
margin-bottom: 24px;
color: #1f2937;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #374151;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #3b82f6;
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.login-btn:hover {
background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
transform: translateY(-2px);
}
.navigation-examples h3 {
margin-bottom: 16px;
color: #1f2937;
}
.nav-btn {
display: block;
width: 100%;
margin-bottom: 12px;
padding: 12px 16px;
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 8px;
color: #374151;
cursor: pointer;
transition: all 0.3s ease;
}
.nav-btn:hover {
background: #e2e8f0;
border-color: #cbd5e1;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
🔄 router.replace() - 替换当前位置
router.replace() 与 router.push() 类似,但不会在历史栈中添加新记录,而是替换当前记录。
vue
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
// 🔄 替换当前路由
function replaceRoute() {
router.replace({ name: 'new-page' })
}
// 🎯 等价写法
function replaceWithPush() {
router.push({ name: 'new-page', replace: true })
}
// 🚀 实际应用场景
function handleFormSubmit() {
// 表单提交成功后,替换当前页面为成功页面
// 这样用户点击后退按钮时不会回到表单页面
router.replace({
name: 'form-success',
query: { message: '提交成功' }
})
}
// 🔐 登录重定向
function redirectAfterLogin() {
// 登录成功后替换登录页面,防止用户后退到登录页
const redirectTo = route.query.redirect || '/dashboard'
router.replace(redirectTo)
}
</script>
<template>
<div class="replace-examples">
<h3>🔄 Replace 导航示例</h3>
<button @click="replaceRoute()" class="btn btn-primary">
🔄 替换当前页面
</button>
<button @click="handleFormSubmit()" class="btn btn-success">
📝 提交表单(替换)
</button>
<button @click="redirectAfterLogin()" class="btn btn-info">
🔐 登录重定向
</button>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
⏭️ 历史导航 - router.go()、router.back()、router.forward()
控制浏览器历史记录的前进和后退。
vue
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const historyStep = ref(1)
// ⬅️ 后退一步
function goBack() {
router.back()
// 等价于 router.go(-1)
}
// ➡️ 前进一步
function goForward() {
router.forward()
// 等价于 router.go(1)
}
// 🎯 自定义步数
function goSteps(steps) {
router.go(steps)
}
// 🔄 智能导航历史
function smartHistoryNavigation() {
// 检查是否有历史记录
if (window.history.length > 1) {
router.back()
} else {
// 没有历史记录,跳转到首页
router.push({ name: 'home' })
}
}
// 📱 移动端友好的返回
function mobileBack() {
// 在移动端,优先使用物理返回键的行为
if (window.history.length > 1) {
router.back()
} else {
// 如果是直接访问的页面,返回到合适的父级页面
const currentRoute = router.currentRoute.value
if (currentRoute.name?.includes('product-detail')) {
router.push({ name: 'product-list' })
} else if (currentRoute.name?.includes('user-')) {
router.push({ name: 'user-profile' })
} else {
router.push({ name: 'home' })
}
}
}
</script>
<template>
<div class="history-navigation">
<h3>📚 历史导航控制</h3>
<!-- 🎮 基础控制 -->
<div class="basic-controls">
<button @click="goBack()" class="btn btn-outline">
⬅️ 后退
</button>
<button @click="goForward()" class="btn btn-outline">
➡️ 前进
</button>
</div>
<!-- 🎯 自定义步数 -->
<div class="custom-steps">
<label for="steps">跳转步数:</label>
<input
id="steps"
v-model.number="historyStep"
type="number"
min="-10"
max="10"
class="step-input"
/>
<button @click="goSteps(historyStep)" class="btn btn-primary">
🎯 跳转 {{ historyStep }} 步
</button>
</div>
<!-- 📱 智能返回 -->
<div class="smart-controls">
<button @click="smartHistoryNavigation()" class="btn btn-success">
🧠 智能返回
</button>
<button @click="mobileBack()" class="btn btn-info">
📱 移动端返回
</button>
</div>
</div>
</template>
<style scoped>
.history-navigation {
padding: 24px;
background: #f8fafc;
border-radius: 12px;
margin: 20px 0;
}
.basic-controls,
.custom-steps,
.smart-controls {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.step-input {
width: 80px;
padding: 8px 12px;
border: 2px solid #e5e7eb;
border-radius: 6px;
text-align: center;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-outline {
background: transparent;
color: #374151;
border: 2px solid #d1d5db;
}
.btn-outline:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-info {
background: #0ea5e9;
color: white;
}
.btn-info:hover {
background: #0284c7;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
🎯 高级用法
🔄 异步导航与 Promise
所有导航方法都返回 Promise,让你能够处理导航的成功或失败。
vue
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isNavigating = ref(false)
const navigationError = ref(null)
// 🚀 异步导航处理
async function navigateWithFeedback(to) {
try {
isNavigating.value = true
navigationError.value = null
// 🎯 执行导航
await router.push(to)
// ✅ 导航成功
showSuccess('页面跳转成功!')
} catch (error) {
// ❌ 导航失败(可能被导航守卫阻止)
navigationError.value = error.message
showError(`导航失败: ${error.message}`)
} finally {
isNavigating.value = false
}
}
// 🎮 批量导航
async function navigateSequence(routes) {
for (const route of routes) {
try {
await router.push(route)
// ⏰ 每次导航间隔
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.error(`导航到 ${route} 失败:`, error)
break
}
}
}
// 🔄 重试导航
async function retryNavigation(to, maxRetries = 3) {
let retries = 0
while (retries < maxRetries) {
try {
await router.push(to)
return // 成功,退出重试循环
} catch (error) {
retries++
console.warn(`导航重试 ${retries}/${maxRetries}:`, error)
if (retries >= maxRetries) {
throw new Error(`导航失败,已重试 ${maxRetries} 次`)
}
// ⏰ 重试前等待
await new Promise(resolve => setTimeout(resolve, 1000 * retries))
}
}
}
// 🎯 条件导航链
async function conditionalNavigationChain() {
try {
// 1. 检查用户权限
const hasPermission = await checkUserPermission()
if (!hasPermission) {
await router.push({ name: 'access-denied' })
return
}
// 2. 预加载数据
const data = await preloadData()
if (!data) {
await router.push({ name: 'data-error' })
return
}
// 3. 最终导航
await router.push({
name: 'target-page',
params: { id: data.id },
state: { preloadedData: data }
})
} catch (error) {
// 🚨 任何步骤失败都跳转到错误页面
await router.push({
name: 'error',
query: { message: error.message }
})
}
}
</script>
<template>
<div class="async-navigation">
<h3>🔄 异步导航示例</h3>
<!-- 🎯 基础异步导航 -->
<div class="nav-section">
<button
@click="navigateWithFeedback({ name: 'user-profile' })"
:disabled="isNavigating"
class="btn btn-primary"
>
<span v-if="isNavigating">🔄 跳转中...</span>
<span v-else>👤 查看个人资料</span>
</button>
<div v-if="navigationError" class="error-message">
❌ {{ navigationError }}
</div>
</div>
<!-- 🎮 批量导航 -->
<div class="nav-section">
<button
@click="navigateSequence([
{ name: 'page1' },
{ name: 'page2' },
{ name: 'page3' }
])"
class="btn btn-secondary"
>
🎮 批量导航演示
</button>
</div>
<!-- 🔄 重试导航 -->
<div class="nav-section">
<button
@click="retryNavigation({ name: 'unstable-page' })"
class="btn btn-warning"
>
🔄 重试导航
</button>
</div>
<!-- 🎯 条件导航链 -->
<div class="nav-section">
<button
@click="conditionalNavigationChain()"
class="btn btn-success"
>
🎯 智能导航链
</button>
</div>
</div>
</template>
<style scoped>
.async-navigation {
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.nav-section {
margin-bottom: 20px;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
}
.error-message {
margin-top: 12px;
padding: 12px;
background: #fef2f2;
color: #dc2626;
border-radius: 6px;
border-left: 4px solid #ef4444;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-warning {
background: #f59e0b;
color: white;
}
.btn-success {
background: #10b981;
color: white;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
🎨 导航状态管理
vue
<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 🎯 导航状态
const navigationState = ref({
isNavigating: false,
previousRoute: null,
navigationHistory: [],
pendingNavigation: null
})
// 📊 导航统计
const navigationStats = computed(() => ({
totalNavigations: navigationState.value.navigationHistory.length,
uniquePages: new Set(navigationState.value.navigationHistory.map(nav => nav.name)).size,
averageTimeOnPage: calculateAverageTimeOnPage(),
mostVisitedPage: getMostVisitedPage()
}))
// 🔍 监听路由变化
watch(route, (newRoute, oldRoute) => {
if (oldRoute) {
// 📝 记录导航历史
navigationState.value.navigationHistory.push({
from: oldRoute.name,
to: newRoute.name,
timestamp: Date.now(),
params: newRoute.params,
query: newRoute.query
})
navigationState.value.previousRoute = oldRoute
}
}, { immediate: true })
// 🎯 智能导航函数
function smartNavigate(to, options = {}) {
const {
confirm = false,
preload = false,
analytics = true,
fallback = null
} = options
return new Promise(async (resolve, reject) => {
try {
// 🔍 确认导航
if (confirm) {
const confirmed = await showConfirmDialog({
title: '确认导航',
message: `确定要跳转到 ${to.name || to} 吗?`,
confirmText: '确定',
cancelText: '取消'
})
if (!confirmed) {
reject(new Error('用户取消导航'))
return
}
}
// 📊 预加载数据
if (preload && to.name) {
await preloadRouteData(to.name)
}
// 🎯 执行导航
navigationState.value.isNavigating = true
navigationState.value.pendingNavigation = to
await router.push(to)
// 📈 发送分析数据
if (analytics) {
sendNavigationAnalytics(to)
}
resolve()
} catch (error) {
// 🚨 导航失败,尝试回退方案
if (fallback) {
try {
await router.push(fallback)
resolve()
} catch (fallbackError) {
reject(fallbackError)
}
} else {
reject(error)
}
} finally {
navigationState.value.isNavigating = false
navigationState.value.pendingNavigation = null
}
})
}
// 📊 计算页面平均停留时间
function calculateAverageTimeOnPage() {
const history = navigationState.value.navigationHistory
if (history.length < 2) return 0
const times = []
for (let i = 1; i < history.length; i++) {
times.push(history[i].timestamp - history[i - 1].timestamp)
}
return times.reduce((sum, time) => sum + time, 0) / times.length
}
// 🏆 获取最常访问的页面
function getMostVisitedPage() {
const history = navigationState.value.navigationHistory
const pageCount = {}
history.forEach(nav => {
pageCount[nav.to] = (pageCount[nav.to] || 0) + 1
})
return Object.entries(pageCount)
.sort(([,a], [,b]) => b - a)[0]?.[0] || null
}
// 🔄 导航回退
function navigateBack(steps = 1) {
const history = navigationState.value.navigationHistory
if (history.length >= steps) {
const targetNav = history[history.length - steps]
router.push({
name: targetNav.from,
params: targetNav.params,
query: targetNav.query
})
} else {
router.push({ name: 'home' })
}
}
</script>
<template>
<div class="navigation-manager">
<div class="navigation-status">
<h3>🧭 导航状态管理</h3>
<!-- 🎯 当前状态 -->
<div class="status-card">
<div class="status-item">
<span class="label">当前页面:</span>
<span class="value">{{ route.name || '未知' }}</span>
</div>
<div class="status-item">
<span class="label">上一页面:</span>
<span class="value">{{ navigationState.previousRoute?.name || '无' }}</span>
</div>
<div class="status-item">
<span class="label">导航状态:</span>
<span class="value" :class="{ 'navigating': navigationState.isNavigating }">
{{ navigationState.isNavigating ? '🔄 导航中' : '✅ 就绪' }}
</span>
</div>
</div>
<!-- 📊 导航统计 -->
<div class="stats-card">
<h4>📊 导航统计</h4>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ navigationStats.totalNavigations }}</div>
<div class="stat-label">总导航次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ navigationStats.uniquePages }}</div>
<div class="stat-label">访问页面数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ Math.round(navigationStats.averageTimeOnPage / 1000) }}s</div>
<div class="stat-label">平均停留时间</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ navigationStats.mostVisitedPage || '无' }}</div>
<div class="stat-label">最常访问</div>
</div>
</div>
</div>
<!-- 🎮 智能导航控制 -->
<div class="controls-card">
<h4>🎮 智能导航</h4>
<div class="control-buttons">
<button
@click="smartNavigate({ name: 'user-profile' }, { confirm: true, preload: true })"
class="btn btn-primary"
>
👤 智能跳转个人资料
</button>
<button
@click="smartNavigate({ name: 'admin-panel' }, {
confirm: true,
fallback: { name: 'access-denied' }
})"
class="btn btn-warning"
>
⚙️ 尝试访问管理面板
</button>
<button
@click="navigateBack()"
class="btn btn-secondary"
>
⬅️ 智能返回
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.navigation-manager {
padding: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px;
margin: 20px 0;
}
.status-card,
.stats-card,
.controls-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
backdrop-filter: blur(10px);
}
.status-item {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.label {
font-weight: 500;
opacity: 0.8;
}
.value {
font-weight: 600;
}
.value.navigating {
color: #fbbf24;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
}
.stat-item {
text-align: center;
padding: 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
opacity: 0.8;
}
.control-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.2);
color: white;
}
.btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
h3, h4 {
margin-bottom: 16px;
color: #fbbf24;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
🎯 实战案例
🛍️ 电商购物流程
vue
<script setup>
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 🛒 购物流程状态
const shoppingFlow = ref({
currentStep: 'browse',
cart: [],
user: null,
shippingAddress: null,
paymentMethod: null
})
// 🎯 购物流程步骤
const flowSteps = {
browse: { name: 'product-list', title: '浏览商品', icon: '🛍️' },
detail: { name: 'product-detail', title: '商品详情', icon: '📱' },
cart: { name: 'shopping-cart', title: '购物车', icon: '🛒' },
login: { name: 'login', title: '登录', icon: '🔐' },
address: { name: 'shipping-address', title: '收货地址', icon: '📍' },
payment: { name: 'payment-method', title: '支付方式', icon: '💳' },
checkout: { name: 'checkout', title: '确认订单', icon: '📋' },
success: { name: 'order-success', title: '订单成功', icon: '🎉' }
}
// 🚀 智能购物导航
async function navigateShoppingFlow(targetStep, productId = null) {
try {
const currentStep = shoppingFlow.value.currentStep
// 🎯 根据目标步骤执行不同逻辑
switch (targetStep) {
case 'detail':
if (!productId) {
throw new Error('商品ID不能为空')
}
await router.push({
name: 'product-detail',
params: { id: productId }
})
break
case 'cart':
await router.push({ name: 'shopping-cart' })
break
case 'checkout':
// 🔍 检查购物流程前置条件
const checkResult = await checkCheckoutPrerequisites()
if (!checkResult.success) {
await handleCheckoutError(checkResult.error)
return
}
await router.push({ name: 'checkout' })
break
case 'login':
await router.push({
name: 'login',
query: {
redirect: route.fullPath,
flow: 'shopping'
}
})
break
default:
await router.push({ name: flowSteps[targetStep].name })
}
shoppingFlow.value.currentStep = targetStep
} catch (error) {
showError(`导航失败: ${error.message}`)
}
}
// 🔍 检查结算前置条件
async function checkCheckoutPrerequisites() {
// 1. 检查购物车
if (shoppingFlow.value.cart.length === 0) {
return {
success: false,
error: 'empty_cart',
message: '购物车为空'
}
}
// 2. 检查登录状态
if (!shoppingFlow.value.user) {
return {
success: false,
error: 'not_logged_in',
message: '请先登录'
}
}
// 3. 检查收货地址
if (!shoppingFlow.value.shippingAddress) {
return {
success: false,
error: 'no_address',
message: '请设置收货地址'
}
}
// 4. 检查支付方式
if (!shoppingFlow.value.paymentMethod) {
return {
success: false,
error: 'no_payment',
message: '请选择支付方式'
}
}
return { success: true }
}
// 🚨 处理结算错误
async function handleCheckoutError(errorType) {
const errorHandlers = {
empty_cart: () => navigateShoppingFlow('browse'),
not_logged_in: () => navigateShoppingFlow('login'),
no_address: () => navigateShoppingFlow('address'),
no_payment: () => navigateShoppingFlow('payment')
}
const handler = errorHandlers[errorType]
if (handler) {
await handler()
}
}
// 🎯 添加商品到购物车并导航
async function addToCartAndNavigate(product) {
try {
// 🛒 添加到购物车
shoppingFlow.value.cart.push(product)
// 🎉 显示成功消息
showSuccess(`${product.name} 已添加到购物车`)
// 🎯 询问用户下一步操作
const action = await showActionDialog({
title: '商品已添加',
message: '您想要继续购物还是查看购物车?',
actions: [
{ text: '继续购物', value: 'continue', icon: '🛍️' },
{ text: '查看购物车', value: 'cart', icon: '🛒' },
{ text: '立即结算', value: 'checkout', icon: '💳' }
]
})
switch (action) {
case 'continue':
// 继续在当前页面
break
case 'cart':
await navigateShoppingFlow('cart')
break
case 'checkout':
await navigateShoppingFlow('checkout')
break
}
} catch (error) {
showError(`添加商品失败: ${error.message}`)
}
}
// 🎮 快速购买流程
async function quickBuy(productId) {
try {
// 1. 跳转到商品详情
await navigateShoppingFlow('detail', productId)
// 2. 模拟添加到购物车
await new Promise(resolve => setTimeout(resolve, 500))
// 3. 直接进入结算流程
await navigateShoppingFlow('checkout')
} catch (error) {
showError(`快速购买失败: ${error.message}`)
}
}
</script>
<template>
<div class="shopping-flow">
<h3>🛍️ 智能购物导航</h3>
<!-- 🎯 流程指示器 -->
<div class="flow-indicator">
<div
v-for="(step, key) in flowSteps"
:key="key"
:class="['flow-step', {
active: shoppingFlow.currentStep === key,
completed: isStepCompleted(key)
}]"
@click="navigateShoppingFlow(key)"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-title">{{ step.title }}</div>
</div>
</div>
<!-- 🛒 购物车状态 -->
<div class="cart-status">
<div class="cart-info">
<span class="cart-icon">🛒</span>
<span class="cart-count">{{ shoppingFlow.cart.length }}</span>
<span class="cart-text">件商品</span>
</div>
<button
@click="navigateShoppingFlow('cart')"
class="btn btn-primary"
>
查看购物车
</button>
</div>
<!-- 🎮 快捷操作 -->
<div class="quick-actions">
<h4>🚀 快捷操作</h4>
<div class="action-grid">
<button
@click="navigateShoppingFlow('browse')"
class="action-btn"
>
<div class="action-icon">🛍️</div>
<div class="action-text">浏览商品</div>
</button>
<button
@click="quickBuy('iphone-15')"
class="action-btn"
>
<div class="action-icon">⚡</div>
<div class="action-text">快速购买</div>
</button>
<button
@click="navigateShoppingFlow('checkout')"
class="action-btn"
>
<div class="action-icon">💳</div>
<div class="action-text">立即结算</div>
</button>
<button
@click="addToCartAndNavigate({
id: 'demo-product',
name: '演示商品',
price: 99
})"
class="action-btn"
>
<div class="action-icon">➕</div>
<div class="action-text">添加商品</div>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.shopping-flow {
padding: 24px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.flow-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 32px;
padding: 20px;
background: #f8fafc;
border-radius: 12px;
overflow-x: auto;
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 80px;
}
.flow-step:hover {
background: #e2e8f0;
}
.flow-step.active {
background: #3b82f6;
color: white;
}
.flow-step.completed {
background: #10b981;
color: white;
}
.step-icon {
font-size: 24px;
margin-bottom: 8px;
}
.step-title {
font-size: 12px;
text-align: center;
font-weight: 500;
}
.cart-status {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
margin-bottom: 24px;
}
.cart-info {
display: flex;
align-items: center;
gap: 8px;
}
.cart-icon {
font-size: 24px;
}
.cart-count {
font-size: 20px;
font-weight: bold;
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
}
.quick-actions h4 {
margin-bottom: 16px;
color: #1f2937;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 16px;
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.action-btn:hover {
background: #e2e8f0;
border-color: #cbd5e1;
transform: translateY(-2px);
}
.action-icon {
font-size: 32px;
margin-bottom: 8px;
}
.action-text {
font-size: 14px;
font-weight: 500;
color: #374151;
}
.btn {
padding: 10px 20px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 8px;
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
🎯 最佳实践
✅ 推荐做法
🎯 使用命名路由
javascript// ✅ 推荐 - 使用命名路由 router.push({ name: 'user-profile', params: { id: userId } }) // ❌ 不推荐 - 硬编码路径 router.push(`/user/${userId}`)1
2
3
4
5🔄 处理导航 Promise
javascript// ✅ 推荐 - 处理异步结果 try { await router.push({ name: 'target-page' }) console.log('导航成功') } catch (error) { console.error('导航失败:', error) } // ❌ 不推荐 - 忽略 Promise router.push({ name: 'target-page' })1
2
3
4
5
6
7
8
9
10🎮 合理使用 replace
javascript// ✅ 表单提交后使用 replace router.replace({ name: 'success-page' }) // ✅ 登录重定向使用 replace router.replace(redirectPath)1
2
3
4
5
⚠️ 注意事项
- 参数与路径冲突 -
params不能与path同时使用 - 历史记录限制 -
router.go()超出范围会静默失败 - 导航守卫 - 可能会阻止导航执行
- 异步操作 - 记得处理 Promise 的成功和失败状态
🚀 下一步学习
掌握了编程式导航后,建议继续学习:
🎉 恭喜!你已经掌握了编程式导航
现在你可以用代码精确控制用户的导航体验了!让路由跳转更智能、更流畅 🚀