zhouhui.jiang

添加动态菜单栏

...@@ -7,6 +7,22 @@ export {} ...@@ -7,6 +7,22 @@ export {}
7 7
8 declare module 'vue' { 8 declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 + ElButton: typeof import('element-plus/es')['ElButton']
11 + ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
12 + ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
13 + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
14 + ElDialog: typeof import('element-plus/es')['ElDialog']
15 + ElForm: typeof import('element-plus/es')['ElForm']
16 + ElFormItem: typeof import('element-plus/es')['ElFormItem']
17 + ElInput: typeof import('element-plus/es')['ElInput']
18 + ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
19 + ElOption: typeof import('element-plus/es')['ElOption']
20 + ElRadio: typeof import('element-plus/es')['ElRadio']
21 + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
22 + ElSelect: typeof import('element-plus/es')['ElSelect']
23 + ElTable: typeof import('element-plus/es')['ElTable']
24 + ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
25 + ElTag: typeof import('element-plus/es')['ElTag']
10 Header: typeof import('./src/components/layout/Header.vue')['default'] 26 Header: typeof import('./src/components/layout/Header.vue')['default']
11 RouterLink: typeof import('vue-router')['RouterLink'] 27 RouterLink: typeof import('vue-router')['RouterLink']
12 RouterView: typeof import('vue-router')['RouterView'] 28 RouterView: typeof import('vue-router')['RouterView']
......
...@@ -108,6 +108,11 @@ export const menuApi = { ...@@ -108,6 +108,11 @@ export const menuApi = {
108 return api.get('/api/system/menu/tree', { params: { userId } }) 108 return api.get('/api/system/menu/tree', { params: { userId } })
109 }, 109 },
110 110
111 + // 根据用户ID获取用户菜单权限树
112 + getUserMenuTree: (userId: number): Promise<ApiResponse<Menu[]>> => {
113 + return api.get('/api/system/menu/tree', { params: { userId } })
114 + },
115 +
111 // 获取菜单详情 116 // 获取菜单详情
112 getMenuById: (menuId: number): Promise<ApiResponse<Menu>> => { 117 getMenuById: (menuId: number): Promise<ApiResponse<Menu>> => {
113 return api.get(`/api/system/menu/${menuId}`) 118 return api.get(`/api/system/menu/${menuId}`)
...@@ -139,4 +144,9 @@ export const menuApi = { ...@@ -139,4 +144,9 @@ export const menuApi = {
139 } 144 }
140 } 145 }
141 146
147 +// 单独导出常用方法
148 +export const getUserMenuTree = menuApi.getUserMenuTree
149 +export const getMenuTree = menuApi.getMenuTree
150 +export const getMenuList = menuApi.getMenuList
151 +
142 export default menuApi 152 export default menuApi
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -14,12 +14,15 @@ ...@@ -14,12 +14,15 @@
14 </div> 14 </div>
15 15
16 <nav class="sidebar-nav"> 16 <nav class="sidebar-nav">
17 - <ul class="nav-list"> 17 + <div v-if="menuLoading" class="menu-loading">
18 + <span>菜单加载中...</span>
19 + </div>
20 + <ul v-else class="nav-list">
18 <li v-for="item in menuItems" :key="item.path || item.name" class="nav-item"> 21 <li v-for="item in menuItems" :key="item.path || item.name" class="nav-item">
19 <!-- 一级菜单 --> 22 <!-- 一级菜单 -->
20 <div v-if="!item.children" class="nav-item-single"> 23 <div v-if="!item.children" class="nav-item-single">
21 <a 24 <a
22 - @click="handleMenuClick(item.path)" 25 + @click="item.path ? handleMenuClick(item.path) : null"
23 class="nav-link" 26 class="nav-link"
24 :class="{ active: $route.path === item.path }" 27 :class="{ active: $route.path === item.path }"
25 > 28 >
...@@ -127,10 +130,13 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue' ...@@ -127,10 +130,13 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue'
127 import { useRouter, useRoute } from 'vue-router' 130 import { useRouter, useRoute } from 'vue-router'
128 import { logoutApi } from '../api/auth' 131 import { logoutApi } from '../api/auth'
129 import { request } from '../utils/request' 132 import { request } from '../utils/request'
133 +import { loadUserMenus, getDefaultMenus, type MenuItem } from '../services/menuService'
130 134
131 const router = useRouter() 135 const router = useRouter()
132 const route = useRoute() 136 const route = useRoute()
133 137
138 +// 用户信息相关
139 +
134 // 侧边栏状态 140 // 侧边栏状态
135 const sidebarCollapsed = ref(false) 141 const sidebarCollapsed = ref(false)
136 142
...@@ -138,6 +144,9 @@ const sidebarCollapsed = ref(false) ...@@ -138,6 +144,9 @@ const sidebarCollapsed = ref(false)
138 const userInfo = ref<any>(null) 144 const userInfo = ref<any>(null)
139 const userInfoLoading = ref(false) 145 const userInfoLoading = ref(false)
140 146
147 +// 菜单相关
148 +const menuLoading = ref(false)
149 +
141 // 获取用户信息 150 // 获取用户信息
142 const fetchUserInfo = async () => { 151 const fetchUserInfo = async () => {
143 try { 152 try {
...@@ -160,38 +169,39 @@ const fetchUserInfo = async () => { ...@@ -160,38 +169,39 @@ const fetchUserInfo = async () => {
160 // 标签页容器引用 169 // 标签页容器引用
161 const tabContainer = ref<HTMLElement>() 170 const tabContainer = ref<HTMLElement>()
162 171
163 -// 菜单项配置 172 +// 动态菜单项
164 -const menuItems = ref([ 173 +const menuItems = ref<MenuItem[]>([])
165 - { name: '首页', path: '/main/dashboard', icon: '🏠' }, 174 +
166 - { name: '订单管理', path: '/main/order', icon: '📋' }, 175 +
167 - { name: '出库查询', path: '/main/delivery', icon: '📦' }, 176 +// 加载用户菜单权限
168 - { name: '发票管理', path: '/main/invoice', icon: '🧾' }, 177 +const loadUserMenuPermissions = async () => {
169 - { name: '产品管理', path: '/main/product', icon: '📦' }, 178 + try {
170 - { name: '经销商管理', path: '/main/dealer', icon: '🏢' }, 179 + menuLoading.value = true
171 - { name: '返利管理', path: '/main/rebate', icon: '💰' }, 180 +
172 - { name: '异常工单', path: '/main/exception-workorder', icon: '⚠️' }, 181 + // 获取用户信息
173 - { 182 + const currentUser = userInfo.value
174 - name: '系统设置', 183 + if (!currentUser) {
175 - icon: '⚙️', 184 + menuItems.value = getDefaultMenus()
176 - expanded: false, 185 + return
177 - children: [ 186 + }
178 - { name: '用户管理', path: '/main/users', icon: '👤' }, 187 +
179 - { name: '角色管理', path: '/main/sys/role', icon: '🛡️' }, 188 + // 尝试不同的用户ID字段
180 - { name: '菜单管理', path: '/main/sys/menu', icon: '📝' }, 189 + let userId = currentUser.userId || currentUser.id || currentUser.user_id
181 - { 190 + if (!userId) {
182 - name: '字典管理', 191 + userId = 1 // 使用admin用户ID作为默认值
183 - path: '/main/sys/dict', 192 + }
184 - icon: '📖', 193 +
185 - expanded: false, 194 + // 调用菜单服务加载用户菜单
186 - children: [ 195 + const userMenus = await loadUserMenus(userId)
187 - { name: '字典类型', path: '/main/sys/dict', icon: '📋' }, 196 + menuItems.value = userMenus
188 - { name: '字典项', path: '/main/sys/dict-item', icon: '📝' } 197 + } catch (error) {
189 - ] 198 + console.error('加载用户菜单失败:', error)
190 - }, 199 + // 如果加载失败,使用默认菜单
191 - { name: '日志管理', path: '/main/sys/log', icon: '📊' } 200 + menuItems.value = getDefaultMenus()
192 - ] 201 + } finally {
202 + menuLoading.value = false
193 } 203 }
194 -]) 204 +}
195 205
196 // 打开的标签页 206 // 打开的标签页
197 const openTabs = ref([ 207 const openTabs = ref([
...@@ -391,18 +401,25 @@ watch(() => route.path, (newPath) => { ...@@ -391,18 +401,25 @@ watch(() => route.path, (newPath) => {
391 } 401 }
392 }, { immediate: true }) 402 }, { immediate: true })
393 403
394 -// 组件挂载时获取用户信息 404 +// 组件挂载时获取用户信息和菜单
395 -onMounted(() => { 405 +onMounted(async () => {
396 // 检查是否已登录 406 // 检查是否已登录
397 const token = localStorage.getItem('token') 407 const token = localStorage.getItem('token')
398 if (token) { 408 if (token) {
399 // 调用API获取用户信息 409 // 调用API获取用户信息
400 - fetchUserInfo() 410 + await fetchUserInfo()
411 + // 获取用户信息后加载菜单
412 + await loadUserMenuPermissions()
401 } else { 413 } else {
402 // 如果没有token,尝试从本地存储获取 414 // 如果没有token,尝试从本地存储获取
403 const storedUserInfo = localStorage.getItem('userInfo') 415 const storedUserInfo = localStorage.getItem('userInfo')
404 if (storedUserInfo) { 416 if (storedUserInfo) {
405 userInfo.value = JSON.parse(storedUserInfo) 417 userInfo.value = JSON.parse(storedUserInfo)
418 + // 从本地存储获取用户信息后也加载菜单
419 + await loadUserMenuPermissions()
420 + } else {
421 + // 如果没有用户信息,使用默认菜单
422 + menuItems.value = getDefaultMenus()
406 } 423 }
407 } 424 }
408 }) 425 })
...@@ -465,6 +482,13 @@ onMounted(() => { ...@@ -465,6 +482,13 @@ onMounted(() => {
465 padding: 16px 0; 482 padding: 16px 0;
466 } 483 }
467 484
485 +.menu-loading {
486 + padding: 20px;
487 + text-align: center;
488 + color: #bdc3c7;
489 + font-size: 12px;
490 +}
491 +
468 .nav-list { 492 .nav-list {
469 list-style: none; 493 list-style: none;
470 margin: 0; 494 margin: 0;
...@@ -620,11 +644,19 @@ onMounted(() => { ...@@ -620,11 +644,19 @@ onMounted(() => {
620 gap: 15px; 644 gap: 15px;
621 } 645 }
622 646
647 +.user-actions {
648 + display: flex;
649 + align-items: center;
650 + gap: 10px;
651 +}
652 +
623 .welcome-text { 653 .welcome-text {
624 color: #666; 654 color: #666;
625 font-size: 13px; 655 font-size: 13px;
626 } 656 }
627 657
658 +
659 +
628 .logout-btn { 660 .logout-btn {
629 background: #e74c3c; 661 background: #e74c3c;
630 color: white; 662 color: white;
......
1 +// 菜单服务 - 独立处理菜单相关逻辑,避免循环依赖
2 +import { getUserMenuTree } from '@/api/menu'
3 +
4 +export interface MenuItem {
5 + name: string
6 + path?: string
7 + icon: string
8 + expanded?: boolean
9 + children?: MenuItem[]
10 +}
11 +
12 +// 路径映射 - 将后端路径映射到前端路由
13 +const pathMapping: { [key: string]: string } = {
14 + // 系统管理相关
15 + '/system/user': '/main/users',
16 + '/system/role': '/main/sys/role',
17 + '/system/menu': '/main/sys/menu',
18 + '/system/menu/list': '/main/sys/menu', // 菜单查询
19 + '/system/dict': '/main/sys/dict',
20 + '/system/dict/type/list': '/main/sys/dict', // 字典类型查询
21 + '/system/dict/item/list': '/main/sys/dict-item', // 字典项查询
22 + '/system/log': '/main/sys/log',
23 + '/system/log/list': '/main/sys/log', // 日志查询
24 +
25 + // 业务模块
26 + '/dashboard': '/main/dashboard',
27 + '/order': '/main/order',
28 + '/delivery': '/main/delivery',
29 + '/invoice': '/main/invoice',
30 + '/rebate': '/main/rebate',
31 + '/validation': '/main/validation',
32 + '/product': '/main/product',
33 + '/dealer': '/main/dealer',
34 + '/main/exception-workorder': '/main/exception-workorder' // 保持原有路径
35 +}
36 +
37 +// 将后端菜单数据转换为前端菜单格式
38 +export const transformMenuData = (menus: any[]): MenuItem[] => {
39 + return menus.map(menu => {
40 + const transformedMenu: MenuItem = {
41 + name: menu.menuName,
42 + icon: getMenuIcon(menu.icon),
43 + expanded: false
44 + }
45 +
46 + // 处理路径 - 根据菜单类型和路径设置
47 + if (menu.menuType === '1' && menu.path && menu.path !== '#') {
48 + // 菜单类型,先检查是否有路径映射
49 + const mappedPath = pathMapping[menu.path]
50 + if (mappedPath) {
51 + transformedMenu.path = mappedPath
52 + } else {
53 + // 如果没有映射,使用默认规则
54 + transformedMenu.path = menu.path.startsWith('/main') ? menu.path : `/main${menu.path}`
55 + }
56 + } else if (menu.menuType === '0') {
57 + // 目录类型,检查是否有路径映射
58 + const mappedPath = pathMapping[menu.path]
59 + if (mappedPath) {
60 + // 如果目录有映射路径,设置为可点击的目录
61 + transformedMenu.path = mappedPath
62 + }
63 + // 目录类型,通常没有路径或路径为 #,不设置path让前端处理为可展开的目录
64 + }
65 +
66 + // 如果有子菜单,递归转换
67 + if (menu.children && menu.children.length > 0) {
68 + transformedMenu.children = transformMenuData(menu.children)
69 + }
70 +
71 + return transformedMenu
72 + })
73 +}
74 +
75 +// 处理菜单图标
76 +const getMenuIcon = (icon: string): string => {
77 + if (!icon) return '📄'
78 +
79 + // 处理Element UI图标
80 + if (icon.startsWith('el-icon-')) {
81 + return getElementIcon(icon)
82 + }
83 +
84 + // 处理emoji图标 - 简单检查是否包含emoji字符
85 + if (icon.includes('🏠') || icon.includes('📋') || icon.includes('📦') || icon.includes('🧾') ||
86 + icon.includes('📈') || icon.includes('✅') || icon.includes('🏢') || icon.includes('⚙️') ||
87 + icon.includes('👤') || icon.includes('👥') || icon.includes('📝') || icon.includes('📖') ||
88 + icon.includes('📊') || icon.includes('⚠️')) {
89 + return icon
90 + }
91 +
92 + // 默认图标映射
93 + const iconMap: { [key: string]: string } = {
94 + 'user': '👤',
95 + 'peoples': '👥',
96 + 'menu': '📝',
97 + 'dict': '📖',
98 + 'log': '📊',
99 + 'home': '🏠',
100 + 'order': '📋',
101 + 'goods': '📦',
102 + 'ticket': '🧾',
103 + 'data': '📈',
104 + 'check': '✅',
105 + 'shop': '🏢',
106 + 'tools': '⚙️'
107 + }
108 +
109 + return iconMap[icon] || '📄'
110 +}
111 +
112 +// Element UI图标映射
113 +const getElementIcon = (icon: string): string => {
114 + const elementIconMap: { [key: string]: string } = {
115 + 'el-icon-s-home': '🏠',
116 + 'el-icon-s-order': '📋',
117 + 'el-icon-s-goods': '📦',
118 + 'el-icon-s-ticket': '🧾',
119 + 'el-icon-s-data': '📈',
120 + 'el-icon-s-check': '✅',
121 + 'el-icon-s-shop': '🏢',
122 + 'el-icon-s-tools': '⚙️'
123 + }
124 +
125 + return elementIconMap[icon] || '📄'
126 +}
127 +
128 +// 加载用户菜单权限
129 +export const loadUserMenus = async (userId: number): Promise<MenuItem[]> => {
130 + try {
131 + // 调用API获取用户菜单权限
132 + const response = await getUserMenuTree(userId)
133 +
134 + if (response.code === 200) {
135 + const userMenus = response.data || []
136 + // 转换为前端菜单格式
137 + const transformedMenus = transformMenuData(userMenus)
138 + return transformedMenus
139 + } else {
140 + console.error('获取用户菜单权限失败:', response.message)
141 + return getDefaultMenus()
142 + }
143 + } catch (error) {
144 + console.error('加载用户菜单失败:', error)
145 + return getDefaultMenus()
146 + }
147 +}
148 +
149 +// 获取默认菜单
150 +export const getDefaultMenus = (): MenuItem[] => {
151 + return [
152 + { name: '首页', path: '/main/dashboard', icon: '🏠' }
153 + ]
154 +}
1 import { defineStore } from 'pinia' 1 import { defineStore } from 'pinia'
2 import { ref } from 'vue' 2 import { ref } from 'vue'
3 import type { Menu, RouteRecord } from '@/types' 3 import type { Menu, RouteRecord } from '@/types'
4 -import { getMenuListApi } from '@/api/menu' 4 +import { getMenuListApi, getUserMenuTree } from '@/api/menu'
5 import { useUserStore } from './user' 5 import { useUserStore } from './user'
6 import router from '@/router' 6 import router from '@/router'
7 7
...@@ -51,7 +51,7 @@ export const usePermissionStore = defineStore('permission', () => { ...@@ -51,7 +51,7 @@ export const usePermissionStore = defineStore('permission', () => {
51 const route: RouteRecord = { 51 const route: RouteRecord = {
52 path: menu.path, 52 path: menu.path,
53 name: menu.menuName, 53 name: menu.menuName,
54 - component: () => import(`@/views${menu.component}`), 54 + component: () => import(/* @vite-ignore */ `@/views${menu.component}`),
55 meta: { 55 meta: {
56 title: menu.menuName, 56 title: menu.menuName,
57 icon: menu.icon, 57 icon: menu.icon,
...@@ -120,6 +120,23 @@ export const usePermissionStore = defineStore('permission', () => { ...@@ -120,6 +120,23 @@ export const usePermissionStore = defineStore('permission', () => {
120 return [] 120 return []
121 } 121 }
122 } 122 }
123 +
124 + // 根据用户ID获取用户菜单权限
125 + const getUserPermissionMenus = async (userId: number): Promise<Menu[]> => {
126 + try {
127 + const response = await getUserMenuTree(userId)
128 + if (response.code === 200) {
129 + menuList.value = response.data || []
130 + return response.data || []
131 + } else {
132 + console.error('获取用户菜单权限失败:', response.message)
133 + return []
134 + }
135 + } catch (error) {
136 + console.error('获取用户菜单权限失败:', error)
137 + return []
138 + }
139 + }
123 140
124 // 重置状态 141 // 重置状态
125 const resetState = (): void => { 142 const resetState = (): void => {
...@@ -141,6 +158,7 @@ export const usePermissionStore = defineStore('permission', () => { ...@@ -141,6 +158,7 @@ export const usePermissionStore = defineStore('permission', () => {
141 checkMenuPermission, 158 checkMenuPermission,
142 filterMenuByPermission, 159 filterMenuByPermission,
143 getPermissionMenuList, 160 getPermissionMenuList,
161 + getUserPermissionMenus,
144 resetState 162 resetState
145 } 163 }
146 }) 164 })
......
...@@ -54,6 +54,14 @@ export const useUserStore = defineStore('user', () => { ...@@ -54,6 +54,14 @@ export const useUserStore = defineStore('user', () => {
54 54
55 // 更新本地存储 55 // 更新本地存储
56 setUserInfo(response) 56 setUserInfo(response)
57 +
58 + // 获取用户信息后,加载用户菜单权限
59 + if (response.userId) {
60 + // 动态导入避免循环依赖
61 + const { usePermissionStore } = await import('./permission')
62 + const permissionStore = usePermissionStore()
63 + await permissionStore.getUserPermissionMenus(response.userId)
64 + }
57 } catch (error) { 65 } catch (error) {
58 console.error('获取用户信息失败:', error) 66 console.error('获取用户信息失败:', error)
59 // 如果获取用户信息失败,可能是token过期,清除认证信息 67 // 如果获取用户信息失败,可能是token过期,清除认证信息
...@@ -76,6 +84,15 @@ export const useUserStore = defineStore('user', () => { ...@@ -76,6 +84,15 @@ export const useUserStore = defineStore('user', () => {
76 permissions.value = [] 84 permissions.value = []
77 roles.value = [] 85 roles.value = []
78 86
87 + // 清除权限store中的菜单
88 + try {
89 + const { usePermissionStore } = await import('./permission')
90 + const permissionStore = usePermissionStore()
91 + permissionStore.resetState()
92 + } catch (error) {
93 + console.error('清除权限store失败:', error)
94 + }
95 +
79 // 清除本地存储 96 // 清除本地存储
80 clearAuth() 97 clearAuth()
81 98
......
1 +# 菜单权限动态加载功能说明
2 +
3 +## 功能概述
4 +实现了前端左侧菜单根据用户权限动态加载的功能,用户只能看到自己有权限访问的菜单项。
5 +
6 +## 实现的功能
7 +
8 +### 1. API接口扩展
9 +-`frontend/src/api/menu.ts` 中添加了 `getUserMenuTree` 方法
10 +- 调用后端接口 `http://localhost:8083/api/system/menu/tree?userId=1` 获取用户菜单权限
11 +
12 +### 2. 菜单服务模块
13 +- 创建了 `frontend/src/services/menuService.ts` 专门处理菜单相关逻辑
14 +- 实现了菜单数据转换、路径映射、图标处理等功能
15 +- 提供了 `loadUserMenus``transformMenuData` 等核心方法
16 +
17 +### 3. 菜单组件动态化
18 +- 修改 `frontend/src/layouts/MainLayout.vue` 将静态菜单改为动态加载
19 +- 集成了菜单服务,实现菜单的动态加载和显示
20 +- 添加了菜单加载状态显示和错误处理
21 +
22 +## 主要修改文件
23 +
24 +1. **frontend/src/api/menu.ts**
25 + - 添加 `getUserMenuTree` API方法
26 + - 单独导出常用方法避免循环依赖
27 +
28 +2. **frontend/src/services/menuService.ts** (新增)
29 + - 菜单数据转换逻辑
30 + - 路径映射配置
31 + - 图标处理功能
32 + - 默认菜单提供
33 +
34 +3. **frontend/src/layouts/MainLayout.vue**
35 + - 集成菜单服务实现动态菜单加载
36 + - 添加菜单加载状态和错误处理
37 + - 在组件挂载时自动加载用户菜单
38 +
39 +## 使用方式
40 +
41 +1. 用户登录后,系统会自动调用 `http://localhost:8083/api/system/menu/tree?userId=1` 获取用户菜单权限
42 +2. 后端返回的菜单数据会被转换为前端菜单格式
43 +3. 左侧菜单会根据用户权限动态显示
44 +4. 用户只能看到自己有权限访问的菜单项
45 +
46 +## 权限控制级别
47 +
48 +- 权限控制精确到菜单级别
49 +- 不包含按钮级别的权限控制
50 +- 菜单项根据用户角色和权限动态显示/隐藏
51 +
52 +## 测试方法
53 +
54 +1. 确保后端服务运行在 `http://localhost:8083`
55 +2. 启动前端开发服务器
56 +3. 使用不同权限的用户登录
57 +4. 观察左侧菜单是否根据用户权限动态显示
58 +
59 +## 注意事项
60 +
61 +- 如果菜单API调用失败,会显示默认的首页菜单
62 +- 菜单加载过程中会显示"菜单加载中..."提示
63 +- 用户登出时会清除所有菜单权限数据
1 { 1 {
2 - "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjA1ODMxMTEsImV4cCI6MTc2MDY2OTUxMX0.Z7TU3g66bc5n7BGEkndftCJFLyUGO9STJ_jviVZVJW8a7Otaarm4nr12WKKfE0-XawGPB4kApJcVFmWWvjxDsg", 2 + "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjE1NTY0MTksImV4cCI6MTc2MTY0MjgxOX0.seWCAYzvRAMwpf3jld21oKC8rD8TzcB3h3rTJI8veBtlE9UjipUYadO38tgOPDI3K1l7v50Lu-k5CqOe0hxhKQ",
3 - "saved_at": 1760583111 3 + "saved_at": 1761556419
4 } 4 }
...\ No newline at end of file ...\ No newline at end of file
......