Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
V
Vue-Dashboard
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
高源
Vue-Dashboard
Commits
0a384086
Commit
0a384086
authored
Jun 20, 2025
by
User
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
路由丢失问题解决
parent
072843b7
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
387 additions
and
43 deletions
+387
-43
ROUTE_REFRESH_FIX_v2.md
ROUTE_REFRESH_FIX_v2.md
+128
-0
index.html
dist/index.html
+2
-2
index.ts
src/router/routes/index.ts
+151
-18
index.ts
src/store/modules/auth/index.ts
+8
-0
index.ts
src/store/modules/route/index.ts
+50
-23
route-debug.ts
src/utils/route-debug.ts
+48
-0
No files found.
ROUTE_REFRESH_FIX_v2.md
0 → 100644
View file @
0a384086
# 动态路由刷新修复方案 v2.0
## 问题分析
### 根本原因
1.
**初始化时序问题**
:
`window.uiGlobalConfig`
在页面刷新时可能没有及时初始化
2.
**状态丢失**
: 动态路由存储在内存中,页面刷新后丢失
3.
**依赖缺失**
: 没有可靠的状态恢复机制
### 修复策略
-
✅ 添加localStorage缓存机制
-
✅ 实现智能等待和重试逻辑
-
✅ 提供多级降级方案
-
✅ 增强错误处理和日志
## 修复内容
### 1. 缓存机制 (`src/router/routes/index.ts`)
```
typescript
// 新增功能:
-
cacheDynamicRoutes
():
缓存路由到
localStorage
-
restoreDynamicRoutesFromCache
():
从缓存恢复路由
-
waitForGlobalConfig
():
等待
uiGlobalConfig
初始化
-
clearDynamicRoutesCache
():
清除缓存(登录时调用)
```
### 2. 改进的路由生成 (`generateDynamicRoutes`)
-
支持强制刷新参数
-
优先从缓存恢复
-
智能等待uiGlobalConfig
-
自动缓存新生成的路由
-
完善的错误处理
### 3. 增强的路由初始化 (`src/store/modules/route/index.ts`)
-
initDynamicAuthRoute: 确保动态路由数据可用
-
forceReloadAuthRoute: 强制重新生成(忽略缓存)
-
多级降级方案
### 4. 登录流程优化 (`src/store/modules/auth/index.ts`)
-
登录成功后清除旧缓存
-
确保获取最新路由数据
### 5. 调试工具 (`src/utils/route-debug.ts`)
-
debugRouteState(): 全局调试函数
-
详细的日志记录
## 使用说明
### 开发环境测试
1.
启动项目:
`pnpm dev`
2.
登录系统
3.
导航到任意动态路由页面
4.
刷新页面(F5 或 Ctrl+F5)
5.
检查控制台日志,确认路由恢复成功
### 调试命令
在浏览器控制台中使用:
```
javascript
// 查看路由状态
debugRouteState
()
// 手动清除缓存
localStorage
.
removeItem
(
'dynamic_routes_cache'
)
// 查看缓存内容
JSON
.
parse
(
localStorage
.
getItem
(
'dynamic_routes_cache'
)
||
'{}'
)
```
### 预期日志输出
正常情况下应该看到:
```
📍 从缓存恢复了 X 个动态路由
📍 开始初始化动态认证路由...
✅ 动态认证路由初始化成功
```
异常情况下会看到:
```
⚠️ 动态路由缓存已过期,清除缓存
📍 开始生成动态路由...
✅ 成功生成并缓存了 X 个动态路由
```
## 缓存策略
### 缓存触发条件
-
首次成功生成动态路由
-
uiGlobalConfig.InternalCode 存在
### 缓存失效条件
-
超过24小时
-
InternalCode 不匹配
-
缓存数据格式错误
### 缓存清除时机
-
用户登录成功
-
手动调用 clearDynamicRoutesCache()
## 故障排查
### 如果路由仍然丢失
1.
检查控制台是否有错误信息
2.
运行
`debugRouteState()`
查看状态
3.
检查 localStorage 中是否有缓存数据
4.
确认 uiGlobalConfig.InternalCode 是否正确设置
### 常见问题
1.
**uiGlobalConfig 未初始化**
: 等待最多10秒,超时后报错
2.
**API调用失败**
: 自动重试,最终使用缓存数据
3.
**缓存过期**
: 自动清除并重新生成
## 性能优化
### 缓存优势
-
页面刷新时立即可用,无需等待API
-
减少不必要的API调用
-
提升用户体验
### 内存优化
-
缓存大小限制在合理范围内
-
自动清理过期数据
-
避免内存泄漏
## 向后兼容
-
完全兼容现有代码
-
不影响原有功能
-
渐进式增强
dist/index.html
View file @
0a384086
<!doctype html>
<!doctype html>
<html
lang=
"zh-cmn-Hans"
>
<html
lang=
"zh-cmn-Hans"
>
<head>
<head>
<meta
name=
"buildTime"
content=
"2025-06-19 1
4:14:13
"
>
<meta
name=
"buildTime"
content=
"2025-06-19 1
7:47:20
"
>
<meta
charset=
"UTF-8"
/>
<meta
charset=
"UTF-8"
/>
<link
rel=
"icon"
href=
"/favicon.svg"
/>
<link
rel=
"icon"
href=
"/favicon.svg"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<meta
name=
"color-scheme"
content=
"light dark"
/>
<meta
name=
"color-scheme"
content=
"light dark"
/>
<title>
VueDashboard
</title>
<title>
VueDashboard
</title>
<script
type=
"module"
crossorigin
src=
"/Content/VueDashboardUi/VueDashboard1/assets/index-D
Irm9V-K
.js"
></script>
<script
type=
"module"
crossorigin
src=
"/Content/VueDashboardUi/VueDashboard1/assets/index-D
GGMzivr
.js"
></script>
<link
rel=
"stylesheet"
crossorigin
href=
"/Content/VueDashboardUi/VueDashboard1/assets/index-B2SFJ6Fn.css"
>
<link
rel=
"stylesheet"
crossorigin
href=
"/Content/VueDashboardUi/VueDashboard1/assets/index-B2SFJ6Fn.css"
>
</head>
</head>
<body>
<body>
...
...
src/router/routes/index.ts
View file @
0a384086
...
@@ -190,39 +190,155 @@ const customRoutes: ElegantRoute[] = [
...
@@ -190,39 +190,155 @@ const customRoutes: ElegantRoute[] = [
// 将动态菜单路由存储在全局变量中
// 将动态菜单路由存储在全局变量中
let
dynamicMenuRoutes
:
ElegantRoute
[]
=
[];
let
dynamicMenuRoutes
:
ElegantRoute
[]
=
[];
// 动态路由缓存键名
const
DYNAMIC_ROUTES_CACHE_KEY
=
'dynamic_routes_cache'
;
const
GLOBAL_CONFIG_CACHE_KEY
=
'ui_global_config_cache'
;
// 等待 uiGlobalConfig 初始化的函数
function
waitForGlobalConfig
(
timeout
=
10000
):
Promise
<
any
>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
if
(
window
.
uiGlobalConfig
?.
InternalCode
)
{
resolve
(
window
.
uiGlobalConfig
);
return
;
}
let
attempts
=
0
;
const
maxAttempts
=
timeout
/
100
;
const
checkInterval
=
setInterval
(()
=>
{
attempts
++
;
if
(
window
.
uiGlobalConfig
?.
InternalCode
)
{
clearInterval
(
checkInterval
);
resolve
(
window
.
uiGlobalConfig
);
}
else
if
(
attempts
>=
maxAttempts
)
{
clearInterval
(
checkInterval
);
console
.
warn
(
'等待 uiGlobalConfig 初始化超时'
);
reject
(
new
Error
(
'uiGlobalConfig initialization timeout'
));
}
},
100
);
});
}
// 缓存动态路由到 localStorage
function
cacheDynamicRoutes
(
routes
:
ElegantRoute
[],
globalConfig
:
any
)
{
try
{
const
cacheData
=
{
routes
,
timestamp
:
Date
.
now
(),
internalCode
:
globalConfig
.
InternalCode
,
version
:
'1.0'
// 版本号,用于缓存失效
};
localStorage
.
setItem
(
DYNAMIC_ROUTES_CACHE_KEY
,
JSON
.
stringify
(
cacheData
));
}
catch
(
error
)
{
console
.
warn
(
'缓存动态路由失败:'
,
error
);
}
}
// 从 localStorage 恢复动态路由
function
restoreDynamicRoutesFromCache
():
boolean
{
try
{
const
cached
=
localStorage
.
getItem
(
DYNAMIC_ROUTES_CACHE_KEY
);
if
(
!
cached
)
return
false
;
const
cacheData
=
JSON
.
parse
(
cached
);
// 检查缓存是否过期(24小时)
const
now
=
Date
.
now
();
const
cacheAge
=
now
-
cacheData
.
timestamp
;
const
maxAge
=
24
*
60
*
60
*
1000
;
// 24小时
if
(
cacheAge
>
maxAge
)
{
console
.
log
(
'动态路由缓存已过期,清除缓存'
);
localStorage
.
removeItem
(
DYNAMIC_ROUTES_CACHE_KEY
);
return
false
;
}
// 检查 InternalCode 是否匹配
if
(
window
.
uiGlobalConfig
?.
InternalCode
&&
cacheData
.
internalCode
!==
window
.
uiGlobalConfig
.
InternalCode
)
{
console
.
log
(
'InternalCode 不匹配,清除缓存'
);
localStorage
.
removeItem
(
DYNAMIC_ROUTES_CACHE_KEY
);
return
false
;
}
// 恢复路由数据
if
(
cacheData
.
routes
&&
Array
.
isArray
(
cacheData
.
routes
))
{
dynamicMenuRoutes
=
[...
cacheData
.
routes
];
console
.
log
(
`从缓存恢复了
${
dynamicMenuRoutes
.
length
}
个动态路由`
);
return
true
;
}
return
false
;
}
catch
(
error
)
{
console
.
warn
(
'恢复动态路由缓存失败:'
,
error
);
localStorage
.
removeItem
(
DYNAMIC_ROUTES_CACHE_KEY
);
return
false
;
}
}
// 动态获取菜单数据并生成路由
// 动态获取菜单数据并生成路由
export
async
function
generateDynamicRoutes
()
{
export
async
function
generateDynamicRoutes
(
forceRefresh
=
false
)
{
try
{
try
{
// 如果不是强制刷新,先尝试从缓存中恢复
if
(
!
forceRefresh
)
{
const
cached
=
restoreDynamicRoutesFromCache
();
if
(
cached
)
{
console
.
log
(
'从缓存中恢复动态路由成功'
);
return
;
}
}
// 清空之前的动态路由
// 清空之前的动态路由
dynamicMenuRoutes
=
[];
dynamicMenuRoutes
=
[];
if
(
!
window
.
uiGlobalConfig
?.
InternalCode
)
{
// 等待 uiGlobalConfig 初始化
console
.
warn
(
'InternalCode not found, skipping dynamic route generation'
);
const
globalConfig
=
await
waitForGlobalConfig
();
if
(
!
globalConfig
?.
InternalCode
)
{
console
.
warn
(
'InternalCode not found even after waiting, skipping dynamic route generation'
);
return
;
return
;
}
}
console
.
log
(
'开始生成动态路由...'
);
const
{
data
:
menus
}
=
await
getRootMenu
(
const
{
data
:
menus
}
=
await
getRootMenu
(
`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=
${
window
.
uiG
lobalConfig
.
InternalCode
}
`
`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=
${
g
lobalConfig
.
InternalCode
}
`
);
);
const
MenuThree
=
getMenuThree
(
menus
?.
MenusMain
?.
Results
);
if
(
!
menus
||
!
menus
.
MenusMain
?.
Results
)
{
const
MenuRoot
=
menus
?.
MenuRoot
;
console
.
warn
(
'菜单数据为空,无法生成动态路由'
);
return
;
}
const
MenuThree
=
getMenuThree
(
menus
.
MenusMain
.
Results
);
const
MenuRoot
=
menus
.
MenuRoot
;
// 存储 MenuRoot 到 store
// 存储 MenuRoot 到 store
if
(
MenuRoot
)
{
if
(
MenuRoot
)
{
setTimeout
(()
=>
{
setTimeout
(()
=>
{
const
routeStore
=
useRouteStore
();
try
{
routeStore
.
setMenuRoot
(
MenuRoot
);
const
routeStore
=
useRouteStore
();
routeStore
.
setMenuRoot
(
MenuRoot
);
}
catch
(
error
)
{
console
.
warn
(
'设置 MenuRoot 到 store 失败:'
,
error
);
}
},
100
);
},
100
);
}
}
const
MenuThree2
=
generateRoutes
(
MenuThree
);
const
MenuThree2
=
generateRoutes
(
MenuThree
);
if
(
MenuThree2
.
length
>
0
)
{
if
(
MenuThree2
.
length
>
0
)
{
dynamicMenuRoutes
=
[...
MenuThree2
];
dynamicMenuRoutes
=
[...
MenuThree2
];
// 缓存到 localStorage
cacheDynamicRoutes
(
dynamicMenuRoutes
,
globalConfig
);
console
.
log
(
`成功生成并缓存了
${
MenuThree2
.
length
}
个动态路由`
);
}
else
{
console
.
warn
(
'未生成任何动态路由'
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'Failed to generate dynamic routes:'
,
error
);
console
.
error
(
'生成动态路由失败:'
,
error
);
// 失败时尝试从缓存恢复
restoreDynamicRoutesFromCache
();
}
}
}
}
...
@@ -313,13 +429,30 @@ export function getAuthVueRoutes(routes: ElegantConstRoute[]) {
...
@@ -313,13 +429,30 @@ export function getAuthVueRoutes(routes: ElegantConstRoute[]) {
return
transformElegantRoutesToVueRoutes
(
routes
,
layouts
,
views
);
return
transformElegantRoutesToVueRoutes
(
routes
,
layouts
,
views
);
}
}
// 初始化时生成一次动态路由(兼容性)
// 添加清除缓存的导出函数
// 确保在浏览器环境且必要的全局配置已准备好时才执行
export
function
clearDynamicRoutesCache
()
{
if
(
typeof
window
!==
'undefined'
&&
window
.
uiGlobalConfig
?.
InternalCode
)
{
try
{
// 使用 setTimeout 确保应用完全初始化后再执行
localStorage
.
removeItem
(
DYNAMIC_ROUTES_CACHE_KEY
);
setTimeout
(()
=>
{
console
.
log
(
'动态路由缓存已清除'
);
generateDynamicRoutes
().
catch
(
error
=>
{
}
catch
(
error
)
{
console
.
warn
(
'Initial dynamic route generation failed:'
,
error
);
console
.
warn
(
'清除动态路由缓存失败:'
,
error
);
});
}
},
100
);
}
// 应用启动时尝试恢复动态路由
// 这会在模块加载时立即执行,确保路由尽早可用
if
(
typeof
window
!==
'undefined'
)
{
// 先尝试从缓存恢复
const
restored
=
restoreDynamicRoutesFromCache
();
if
(
!
restored
)
{
// 如果缓存恢复失败,尝试等待 uiGlobalConfig 并生成新路由
setTimeout
(
async
()
=>
{
try
{
await
generateDynamicRoutes
(
false
);
// 不强制刷新,允许从缓存恢复
}
catch
(
error
)
{
console
.
warn
(
'初始动态路由生成失败:'
,
error
);
}
},
500
);
// 增加延迟时间,确保 uiGlobalConfig 有更多时间初始化
}
}
}
src/store/modules/auth/index.ts
View file @
0a384086
...
@@ -100,6 +100,14 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
...
@@ -100,6 +100,14 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
/** Initialize routes after login */
/** Initialize routes after login */
async
function
initializeAuthRoutes
(
redirect
:
boolean
)
{
async
function
initializeAuthRoutes
(
redirect
:
boolean
)
{
// 登录后清除动态路由缓存,确保获取最新数据
try
{
const
{
clearDynamicRoutesCache
}
=
await
import
(
'@/router/routes'
);
clearDynamicRoutesCache
();
}
catch
(
error
)
{
console
.
warn
(
'清除动态路由缓存失败:'
,
error
);
}
await
routeStore
.
forceReloadAuthRoute
();
await
routeStore
.
forceReloadAuthRoute
();
if
(
!
routeStore
.
isInitAuthRoute
)
{
if
(
!
routeStore
.
isInitAuthRoute
)
{
...
...
src/store/modules/route/index.ts
View file @
0a384086
...
@@ -229,6 +229,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
...
@@ -229,6 +229,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
/** Force reload auth route - 强制重新加载路由 */
/** Force reload auth route - 强制重新加载路由 */
async
function
forceReloadAuthRoute
()
{
async
function
forceReloadAuthRoute
()
{
console
.
log
(
'强制重新加载认证路由...'
);
// 重置路由状态
// 重置路由状态
setIsInitAuthRoute
(
false
);
setIsInitAuthRoute
(
false
);
...
@@ -238,11 +240,13 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
...
@@ -238,11 +240,13 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
// 清除现有的auth routes
// 清除现有的auth routes
authRoutes
.
value
=
[];
authRoutes
.
value
=
[];
//
重新生成动态路由数据
//
强制重新生成动态路由数据(忽略缓存)
await
generateDynamicRoutes
();
await
generateDynamicRoutes
(
true
);
// 重新初始化路由
// 重新初始化路由
await
initAuthRoute
();
await
initAuthRoute
();
console
.
log
(
'认证路由重新加载完成'
);
}
}
/** Init static auth route */
/** Init static auth route */
...
@@ -264,38 +268,61 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
...
@@ -264,38 +268,61 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
/** Init dynamic auth route */
/** Init dynamic auth route */
async
function
initDynamicAuthRoute
()
{
async
function
initDynamicAuthRoute
()
{
// 首先尝试使用后端接口获取路由
try
{
const
{
data
,
error
}
=
await
fetchGetUserRoutes
(
);
console
.
log
(
'开始初始化动态认证路由...'
);
if
(
!
error
&&
data
?.
routes
)
{
// 确保动态路由数据可用(从缓存或API)
const
{
routes
,
home
}
=
data
;
await
generateDynamicRoutes
(
false
)
;
addAuthRoutes
(
routes
);
// 首先尝试使用后端接口获取路由
const
{
data
,
error
}
=
await
fetchGetUserRoutes
();
handleConstantAndAuthRoutes
();
if
(
!
error
&&
data
?.
routes
)
{
const
{
routes
,
home
}
=
data
;
setRouteHome
(
home
);
addAuthRoutes
(
routes
);
handleUpdateRootRouteRedirect
(
home
);
handleConstantAndAuthRoutes
(
);
setIsInitAuthRoute
(
true
);
setRouteHome
(
home
);
}
else
{
// 如果后端接口获取失败,使用静态路由模式
console
.
warn
(
'Dynamic route fetch failed, fallback to static routes'
);
// 重新创建静态路由(这会重新调用接口获取最新菜单数据)
handleUpdateRootRouteRedirect
(
home
);
const
{
authRoutes
:
staticAuthRoutes
}
=
createStaticRoutes
();
if
(
authStore
.
isStaticSuper
)
{
setIsInitAuthRoute
(
true
);
addAuthRoutes
(
staticAuthRoutes
);
console
.
log
(
'动态认证路由初始化成功(使用后端接口)'
);
}
else
{
}
else
{
const
filteredAuthRoutes
=
filterAuthRoutesByRoles
(
staticAuthRoutes
,
authStore
.
userInfo
.
roles
);
// 如果后端接口获取失败,使用静态路由模式
addAuthRoutes
(
filteredAuthRoutes
);
console
.
warn
(
'后端接口获取动态路由失败,回退到静态路由模式'
);
}
handleConstantAndAuthRoutes
();
// 创建静态路由(这会使用已生成/恢复的动态菜单路由)
const
{
authRoutes
:
staticAuthRoutes
}
=
createStaticRoutes
();
setIsInitAuthRoute
(
true
);
if
(
authStore
.
isStaticSuper
)
{
addAuthRoutes
(
staticAuthRoutes
);
}
else
{
const
filteredAuthRoutes
=
filterAuthRoutesByRoles
(
staticAuthRoutes
,
authStore
.
userInfo
.
roles
);
addAuthRoutes
(
filteredAuthRoutes
);
}
handleConstantAndAuthRoutes
();
setIsInitAuthRoute
(
true
);
console
.
log
(
'动态认证路由初始化成功(使用静态路由模式)'
);
}
}
catch
(
error
)
{
console
.
error
(
'动态认证路由初始化失败:'
,
error
);
// 发生错误时,尝试使用基本的静态路由确保应用可用
try
{
const
{
authRoutes
:
staticAuthRoutes
}
=
createStaticRoutes
();
addAuthRoutes
(
staticAuthRoutes
);
handleConstantAndAuthRoutes
();
setIsInitAuthRoute
(
true
);
console
.
log
(
'使用基本静态路由作为降级方案'
);
}
catch
(
fallbackError
)
{
console
.
error
(
'降级方案也失败了:'
,
fallbackError
);
throw
fallbackError
;
}
}
}
}
}
...
...
src/utils/route-debug.ts
0 → 100644
View file @
0a384086
/**
* 路由调试工具
* 用于诊断动态路由初始化问题
*/
export
function
debugRouteState
()
{
const
info
=
{
timestamp
:
new
Date
().
toISOString
(),
uiGlobalConfig
:
{
exists
:
!!
window
.
uiGlobalConfig
,
internalCode
:
window
.
uiGlobalConfig
?.
InternalCode
||
null
,
isAuthenticated
:
window
.
uiGlobalConfig
?.
IsAuthenticated
||
null
,
displayName
:
window
.
uiGlobalConfig
?.
DisplayName
||
null
},
localStorage
:
{
token
:
!!
localStorage
.
getItem
(
'token'
),
dynamicRoutesCache
:
!!
localStorage
.
getItem
(
'dynamic_routes_cache'
),
userInfo
:
!!
localStorage
.
getItem
(
'userInfo'
)
},
router
:
{
routes
:
(
window
as
any
).
__VUE_ROUTER__
?.
getRoutes
?.()?.
length
||
0
,
currentRoute
:
(
window
as
any
).
__VUE_ROUTER__
?.
currentRoute
?.
value
?.
name
||
null
}
};
console
.
group
(
'🔍 路由状态调试信息'
);
console
.
table
(
info
);
console
.
groupEnd
();
return
info
;
}
// 在全局暴露调试函数
if
(
typeof
window
!==
'undefined'
)
{
(
window
as
any
).
debugRouteState
=
debugRouteState
;
}
export
function
logRouteInitStep
(
step
:
string
,
details
?:
any
)
{
console
.
log
(
`📍 路由初始化:
${
step
}
`
,
details
||
''
);
}
export
function
logRouteError
(
error
:
string
,
details
?:
any
)
{
console
.
error
(
`❌ 路由错误:
${
error
}
`
,
details
||
''
);
}
export
function
logRouteSuccess
(
message
:
string
,
details
?:
any
)
{
console
.
log
(
`✅ 路由成功:
${
message
}
`
,
details
||
''
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment