Commit 25f87f01 authored by 罗超's avatar 罗超

处理参数中间键

parent f58f29e6
# 开发环境配置 - 使用Mock数据
VITE_OTA_API_BASE_URL=http://192.168.5.39:19002/api/app/
VITE_OTA_API_BASE_URL=http://localhost:19002/api/app/
VITE_ERP_API_BASE_URL=http://192.168.5.214:8700/
VITE_FILEUPLOAD_API_BASE_URL=http://192.168.5.214:8120/
VITE_FILEPREVIEW_API_BASE_URL=http://192.168.5.214:8130/
......
# 短码参数解析使用指南
## 快速开始
### 基础用法
```vue
<script setup lang="ts">
import { useRouteParams } from '@/composables/useRouteParams'
// 自动获取并解析所有参数(包括短码)
const { params, loading } = useRouteParams()
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else>
<p>产品ID: {{ params.productId }}</p>
<p>分类ID: {{ params.categoryId }}</p>
</div>
</template>
```
### 带类型提示
```vue
<script setup lang="ts">
interface PageParams {
productId: string
categoryId: string
}
const { params } = useRouteParams<PageParams>()
// params.value.productId ✅ 有完整类型提示
</script>
```
## 常用配置
### 白名单配置(推荐)
```vue
<script setup lang="ts">
// page、pageSize 等不需要解析的参数
const { params } = useRouteParams({
whitelist: ['page', 'pageSize', 'userId', 'orderId']
})
</script>
```
### 自定义转换
```vue
<script setup lang="ts">
// 短码可能返回多个 GUID(逗号分隔)
const { params } = useRouteParams({
transform: (raw) => ({
...raw,
// 自动拆分为数组
productIds: raw.productIds?.split?.(',') || []
})
})
</script>
```
### 错误处理
```vue
<script setup lang="ts">
const { params, error } = useRouteParams({
fallbackMode: 'useOriginal', // 失败时使用原值
onError: (err) => {
console.error('参数解析失败:', err)
toast.error('参数加载失败')
}
})
</script>
```
## 配置选项
```typescript
useRouteParams({
// 自动刷新(路由变化时)@default true
autoRefresh: true,
// 立即执行 @default true
immediate: true,
// 参数来源 @default { params: true, query: true, meta: ['filters'] }
sources: {
params: true,
query: true,
meta: ['filters']
},
// 白名单:这些参数不解析
whitelist: ['page', 'pageSize'],
// 解析超时(毫秒)@default 5000
timeout: 5000,
// 失败降级 @default 'useOriginal'
fallbackMode: 'useOriginal' | 'throw' | 'empty',
// 自定义转换
transform: (params) => params,
// 错误回调
onError: (error) => {}
})
```
## 返回值
```typescript
const {
params, // 解析后的参数(响应式)
loading, // 加载状态
error, // 错误信息
debugInfo, // 调试信息(开发环境)
refresh, // 手动刷新
get // 获取单个参数
} = useRouteParams()
```
## 高级功能
### 菜单预加载
```typescript
import { preloadMenuShortCodes } from '@/composables/useRouteParams'
// 在菜单加载后预加载所有短码
const menus = await loadMenus()
await preloadMenuShortCodes(menus)
```
### 开发调试
浏览器控制台:
```javascript
// 查看缓存统计
window.__shortCodeResolver.stats()
// 手动解析短码
await window.__shortCodeResolver.resolve('lhg5RP6rnE')
// 清除缓存
window.__shortCodeResolver.clearCache()
```
## 最佳实践
### ✅ 推荐
```vue
<script setup lang="ts">
// 1. 定义类型
interface PageParams {
productId: string
categoryId: string
}
// 2. 配置白名单
const { params } = useRouteParams<PageParams>({
whitelist: ['page', 'pageSize']
})
// 3. 直接使用
watchEffect(() => {
if (params.value.productId) {
loadData(params.value.productId)
}
})
</script>
```
### ❌ 避免
```vue
<script setup lang="ts">
// ❌ 不要手动获取和解析
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params.id // 可能是短码
// ✅ 使用 useRouteParams
const { params } = useRouteParams()
const id = params.value.id // 自动解析
</script>
```
## 常见问题
**Q: 参数没有自动解析?**
A: 检查是否在白名单中,或参数长度小于 6 位。
**Q: 如何提升性能?**
A: 使用 `preloadMenuShortCodes` 预加载菜单短码。
**Q: 如何判断是否被解析?**
A: 查看 `debugInfo.value.resolved` 数组(开发环境)。
## 工作原理
```
用户访问 /products/lhg5RP6rnE
useRouteParams() 收集参数
智能判断并解析短码
缓存结果(30分钟)
返回解析后的参数
```
## 总结
- **零学习成本**:一行代码搞定
- **完全透明**:自动处理所有短码
- **类型安全**:完整 TypeScript 支持
- **高性能**:智能缓存和并发解析
<template>
<div class="short-code-example">
<h2>短码参数解析示例</h2>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<a-spin />
<span>正在解析参数...</span>
</div>
<!-- 错误状态 -->
<a-alert v-else-if="error" type="error" :message="error.message" banner />
<!-- 参数展示 -->
<div v-else class="params-display">
<a-descriptions title="解析后的参数" bordered :column="1">
<a-descriptions-item v-for="(value, key) in params" :key="key" :label="key">
<a-tag v-if="Array.isArray(value)" color="blue">
{{ value.join(', ') }}
</a-tag>
<a-tag v-else-if="typeof value === 'object'" color="green">
{{ JSON.stringify(value) }}
</a-tag>
<a-tag v-else color="orange">{{ value }}</a-tag>
</a-descriptions-item>
</a-descriptions>
<!-- 调试信息 -->
<a-card v-if="debugInfo" title="调试信息" style="margin-top: 20px">
<p>
<strong>解析的短码:</strong>
<a-tag v-for="code in debugInfo.resolved" :key="code" color="purple">
{{ code }}
</a-tag>
</p>
<p>
<strong>解析耗时:</strong>
<a-tag color="cyan">{{ debugInfo.timing }}ms</a-tag>
</p>
<p>
<strong>参数来源:</strong>
</p>
<ul>
<li v-for="(source, key) in debugInfo.sources" :key="key">
<code>{{ key }}</code> 来自 <a-tag>{{ source }}</a-tag>
</li>
</ul>
</a-card>
<!-- 操作按钮 -->
<div class="actions" style="margin-top: 20px">
<a-button type="primary" @click="handleRefresh">刷新参数</a-button>
<a-button @click="handleClearCache">清除缓存</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouteParams } from '@/composables/useRouteParams'
import { shortCodeResolver } from '@/utils/shortCodeResolver'
import { useToast } from 'vue-toastification'
const toast = useToast()
// 基础用法:自动解析所有参数
const { params, loading, error, debugInfo, refresh } = useRouteParams({
// 配置白名单
whitelist: ['page', 'pageSize'],
// 错误处理
onError: (err) => {
console.error('参数解析失败:', err)
toast.error('参数解析失败,请重试')
}
})
// 刷新参数
const handleRefresh = async () => {
try {
await refresh()
toast.success('参数已刷新')
} catch (err) {
toast.error('刷新失败')
}
}
// 清除缓存
const handleClearCache = () => {
shortCodeResolver.clearCache()
toast.success('缓存已清除')
}
</script>
<style scoped>
.short-code-example {
padding: 20px;
}
.loading {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
}
.params-display {
margin-top: 20px;
}
.actions {
display: flex;
gap: 10px;
}
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
}
</style>
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { shortCodeResolver } from '@/utils/shortCodeResolver'
/**
* 参数来源配置
*/
interface ParamSources {
/** 是否包含 route.params */
params?: boolean
/** 是否包含 route.query */
query?: boolean
/** 从 route.meta 中提取的字段 */
meta?: string[]
}
/**
* 调试信息
*/
interface DebugInfo {
/** 被解析的短码列表 */
resolved: string[]
/** 参数来源映射 */
sources: Record<string, 'params' | 'query' | 'meta'>
/** 解析耗时(毫秒) */
timing: number
}
/**
* 配置选项
*/
interface UseRouteParamsOptions<T = any> {
/** 是否自动刷新(路由变化时)@default true */
autoRefresh?: boolean
/** 是否立即执行 @default true */
immediate?: boolean
/** 自定义参数转换函数 */
transform?: (params: Record<string, any>) => T
/** 参数来源配置 @default { params: true, query: true, meta: ['filters'] } */
sources?: ParamSources
/** 参数白名单:这些参数永远不解析短码 */
whitelist?: string[]
/** 解析超时时间(毫秒)@default 5000 */
timeout?: number
/** 失败时的降级模式 @default 'useOriginal' */
fallbackMode?: 'useOriginal' | 'throw' | 'empty'
/** 错误回调 */
onError?: (error: Error) => void
}
/**
* 统一获取路由参数(自动解析短码)
*
* @example
* ```ts
* // 基础用法(响应式)
* const { params, loading } = useRouteParams()
*
* // 使用 await 等待解析完成(推荐!)
* const routeParams = useRouteParams()
* const params = await routeParams.ready
* console.log(params) // 已解析完成的参数
*
* // 带类型提示
* interface PageParams {
* productId: string
* categoryId: string
* }
* const { ready } = useRouteParams<PageParams>()
* const params = await ready
*
* // 自定义配置
* const { params } = useRouteParams({
* whitelist: ['page', 'pageSize'],
* transform: (raw) => ({ ...raw, ids: raw.ids.split(',') })
* })
* ```
*/
export function useRouteParams<T = Record<string, any>>(options: UseRouteParamsOptions<T> = {}) {
const {
autoRefresh = true,
immediate = true,
transform,
sources = {
params: true,
query: true,
meta: ['filters']
},
whitelist = [],
timeout = 5000,
fallbackMode = 'useOriginal',
onError
} = options
const route = useRoute()
const loading = ref(false)
const error = ref<Error | null>(null)
const params = ref<T>({} as T)
const debugInfo = ref<DebugInfo>()
// 用于 await 的 Promise
let readyResolve: ((value: T) => void) | null = null
const ready = new Promise<T>((resolve) => {
readyResolve = resolve
})
/**
* 收集所有来源的参数(优先级:params > query > meta)
*/
const collectParams = (): Record<string, any> => {
const collected: Record<string, any> = {}
const paramSources: Record<string, 'params' | 'query' | 'meta'> = {}
// 从 meta 中提取(优先级最低)
if (sources.meta) {
sources.meta.forEach((key) => {
const metaValue = (route.meta as any)[key]
if (metaValue && typeof metaValue === 'object') {
Object.keys(metaValue).forEach((k) => {
collected[k] = metaValue[k]
paramSources[k] = 'meta'
})
}
})
}
// 从 query 中提取(优先级中等)
if (sources.query) {
Object.entries(route.query).forEach(([key, value]) => {
collected[key] = value
paramSources[key] = 'query'
})
}
// 从 params 中提取(优先级最高)
if (sources.params) {
Object.entries(route.params).forEach(([key, value]) => {
collected[key] = value
paramSources[key] = 'params'
})
}
return collected
}
/**
* 解析单个参数值
*/
const resolveValue = async (key: string, value: any): Promise<any> => {
// 白名单中的参数不解析
if (whitelist.includes(key)) {
return value
}
// 字符串:尝试解析短码
if (typeof value === 'string') {
return await shortCodeResolver.resolveIfNeeded(value, timeout)
}
// 数组:递归解析每个元素
if (Array.isArray(value)) {
return await Promise.all(value.map((v) => resolveValue(key, v)))
}
// 其他类型直接返回
return value
}
/**
* 获取所有参数(自动解析短码)
*/
const getAll = async (): Promise<T> => {
const startTime = Date.now()
const resolvedCodes: string[] = []
loading.value = true
error.value = null
try {
// 1. 收集所有参数
const allParams = collectParams()
// 2. 并发解析所有参数
const resolved: Record<string, any> = {}
await Promise.all(
Object.entries(allParams).map(async ([key, value]) => {
const originalValue = value
resolved[key] = await resolveValue(key, value)
// 记录被解析的短码
if (
typeof originalValue === 'string' &&
resolved[key] !== originalValue &&
!whitelist.includes(key)
) {
resolvedCodes.push(originalValue)
}
})
)
// 3. 自定义转换
const final = transform ? transform(resolved) : (resolved as T)
params.value = final
// 4. 保存调试信息(仅开发环境)
if (import.meta.env.DEV) {
const paramSources: Record<string, 'params' | 'query' | 'meta'> = {}
if (sources.params) {
Object.keys(route.params).forEach((k) => (paramSources[k] = 'params'))
}
if (sources.query) {
Object.keys(route.query).forEach((k) => (paramSources[k] = 'query'))
}
debugInfo.value = {
resolved: resolvedCodes,
sources: paramSources,
timing: Date.now() - startTime
}
}
// 5. 触发 ready Promise
if (readyResolve) {
readyResolve(final)
readyResolve = null // 只触发一次
}
return final
} catch (e) {
const err = e as Error
error.value = err
// 执行错误回调
onError?.(err)
// 降级策略
if (fallbackMode === 'useOriginal') {
params.value = collectParams() as T
return params.value
} else if (fallbackMode === 'throw') {
throw err
} else {
params.value = {} as T
return params.value
}
} finally {
loading.value = false
}
}
/**
* 获取单个参数
*/
const get = async <K extends keyof T>(key: K): Promise<T[K] | undefined> => {
if (Object.keys(params.value).length === 0) {
await getAll()
}
return params.value[key]
}
/**
* 手动刷新参数
*/
const refresh = getAll
// 自动刷新模式
if (autoRefresh) {
watch(() => route.fullPath, getAll, { immediate })
} else if (immediate) {
onMounted(getAll)
}
return {
/** 解析后的参数对象(响应式) */
params: computed(() => params.value),
/** 加载状态 */
loading: computed(() => loading.value),
/** 错误信息 */
error: computed(() => error.value),
/** 调试信息(仅开发环境) */
debugInfo: computed(() => debugInfo.value),
/** 等待首次解析完成的 Promise */
ready,
/** 手动刷新参数 */
refresh,
/** 获取单个参数 */
get
}
}
/**
* 预加载菜单中的短码
*/
export async function preloadMenuShortCodes(menus: Array<{ url?: string; path?: string }>) {
const allCodes = menus.flatMap((menu) => {
const url = menu.url || menu.path || ''
return shortCodeResolver.extractFromUrl(url)
})
await shortCodeResolver.preload(allCodes)
}
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 白名单路由(不需要登录即可访问)
const whiteList = ['/login', '/denied', '/notPermission', '/404', '/ai/excel']
const whiteListRegex = [/\/oa\//]
const webSiteName = 'Viitto CRM 专注于旅行社的客户管理与营销系统'
export const createPermissionGuard = (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
console.log(to.path)
const userStore = useUserStore()
const pageTitle = to.matched.find((item) => item.meta.title)
//const menus = userStore.getFlattenMenus
if (pageTitle) {
document.title = `${pageTitle.meta.title} - ${webSiteName}`
} else {
document.title = webSiteName
}
// 检查是否是白名单路由
if (whiteList.includes(to.path)) {
next()
return
}
if (whiteListRegex.some((regex) => regex.test(to.path))) {
next()
return
}
// 检查用户是否登录
if (!userStore.getUserToken) {
next('/login')
return
}
// console.log(to.path)
// const userStore = useUserStore()
// const pageTitle = to.matched.find((item) => item.meta.title)
// //const menus = userStore.getFlattenMenus
// if (pageTitle) {
// document.title = `${pageTitle.meta.title} - ${webSiteName}`
// } else {
// document.title = webSiteName
// }
// // 检查是否是白名单路由
// if (whiteList.includes(to.path)) {
// next()
// return
// }
// 已登录用户检查权限状态
if (userStore.getDenied) {
next('/denied')
return
}
// if (whiteListRegex.some((regex) => regex.test(to.path))) {
// next()
// return
// }
const isErpMenus = to.fullPath.startsWith('/erp/')
// 已登录且有权限的用户,检查是否已获取用户信息
// if (
// menus.length == 0 ||
// (to.path != '/' && menus.findIndex((x) => x.MenuUrl == to.path) == -1 && !isErpMenus)
// ) {
// next('/notPermission')
// // 检查用户是否登录
// if (!userStore.getUserToken) {
// next('/login')
// return
// }
next()
}
......@@ -11,6 +11,12 @@ const router = createRouter({
component: () => import("../views/auth/Login.vue"),
meta: { title: "page.login" },
},
{
path: "/products/:type/:city",
name: "products",
component: () => import("../views/products/List.vue"),
meta: { title: "page.products" },
},
],
});
......
import Api from '@/api/OtaRequest'
/**
* 数据映射类型枚举
*/
export enum DataMappingTypeEnum {
Url = 'Url',
GuidList = 'GuidList',
Json = 'Json',
Text = 'Text'
}
/**
* 数据映射DTO
*/
export interface DataMappingDto {
id: number
shortCode: string
dataType: DataMappingTypeEnum
value: string
metadata?: string
expireTime?: string
creationTime: string
}
/**
* 创建数据映射DTO
*/
export interface CreateDataMappingDto {
dataType: DataMappingTypeEnum
value: string
metadata?: string
expireTime?: string
}
/**
* 更新数据映射DTO
*/
export interface UpdateDataMappingDto {
dataType?: DataMappingTypeEnum
value?: string
metadata?: string
expireTime?: string
}
/**
* 数据映射服务
*/
class DataMappingService {
/**
* 创建数据映射
*/
static async CreateAsync(dto: CreateDataMappingDto): Promise<DataMappingDto> {
return Api.post('/data-mapping', dto)
}
/**
* 更新数据映射
*/
static async UpdateAsync(shortCode: string, dto: UpdateDataMappingDto): Promise<DataMappingDto> {
return Api.put('/data-mapping', dto, { params: { shortCode } })
}
/**
* 删除数据映射
*/
static async DeleteAsync(shortCode: string): Promise<void> {
return Api.delete('/data-mapping', { params: { shortCode } })
}
/**
* 根据短码获取数据
*/
static async GetByShortCodeAsync(shortCode: string): Promise<DataMappingDto> {
return Api.get('/data-mapping/by-short-code', { shortCode })
}
}
export default DataMappingService
import 'vue-router'
/**
* 扩展 Vue Router 的类型定义
* 为 RouteMeta 添加自定义字段
*/
declare module 'vue-router' {
interface RouteMeta {
/**
* 页面标题(支持 i18n key)
*/
title?: string
/**
* 筛选器参数
* 用于存储页面级的筛选配置
*/
filters?: Record<string, any>
/**
* 页面配置
* 用于存储页面级的自定义配置
*/
config?: Record<string, any>
/**
* 解析后的参数(由 useRouteParams 自动填充)
* @internal
*/
resolvedParams?: Record<string, any>
/**
* 其他自定义字段
*/
[key: string]: any
}
}
export {}
import DataMappingService, { DataMappingTypeEnum } from '@/services/DataMappingService'
/**
* 短码解析结果
*/
interface ResolvedData {
shortCode: string
dataType: DataMappingTypeEnum
value: string
parsed: any
timestamp: number
}
/**
* 短码解析器
* 负责识别、解析和缓存短码
*/
class ShortCodeResolver {
private cache = new Map<string, ResolvedData>()
private readonly TTL = 30 * 60 * 1000 // 30分钟缓存
/**
* 判断字符串是否可能是短码
* Base62 编码特征:字母数字组合,6-1024位,非纯数字
*/
private isLikelyShortCode(value: string): boolean {
if (typeof value !== 'string') return false
// Base62 字符集检查
if (!/^[a-zA-Z0-9]+$/.test(value)) return false
// 长度范围检查
if (value.length < 6 || value.length > 1024) return false
// 排除纯数字(避免误判 ID)
if (/^\d+$/.test(value)) return false
return true
}
/**
* 根据数据类型解析值
*/
private parseValue(dataType: DataMappingTypeEnum, value: string): any {
switch (dataType) {
case DataMappingTypeEnum.GuidList:
return value.split(',')
case DataMappingTypeEnum.Json:
try {
return JSON.parse(value)
} catch {
console.warn('JSON 解析失败,返回原始值:', value)
return value
}
case DataMappingTypeEnum.Text:
case DataMappingTypeEnum.Url:
default:
return value
}
}
/**
* 检查缓存是否过期
*/
private isCacheValid(cached: ResolvedData): boolean {
return Date.now() - cached.timestamp < this.TTL
}
/**
* 解析短码(核心方法)
*/
async resolve(shortCode: string): Promise<ResolvedData> {
// 检查缓存
const cached = this.cache.get(shortCode)
if (cached && this.isCacheValid(cached)) {
return cached
}
// 调用 API 获取数据
const result = await DataMappingService.GetByShortCodeAsync(shortCode)
// 构建解析结果
const resolved: ResolvedData = {
shortCode,
dataType: result.dataType,
value: result.value,
parsed: this.parseValue(result.dataType, result.value),
timestamp: Date.now()
}
// 缓存结果
this.cache.set(shortCode, resolved)
return resolved
}
/**
* 智能解析:如果是短码就解析,否则返回原值
*/
async resolveIfNeeded(value: string, timeout = 5000): Promise<string | any> {
// 快速判断:不是短码直接返回
if (!this.isLikelyShortCode(value)) {
return value
}
try {
// 带超时的解析
const result = await Promise.race([
this.resolve(value),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('解析超时')), timeout)
)
])
return result.parsed
} catch (error) {
// 解析失败,降级返回原值
if (import.meta.env.DEV) {
console.warn('短码解析失败,使用原值:', value, error)
}
return value
}
}
/**
* 批量预加载短码(用于菜单预加载)
*/
async preload(shortCodes: string[]): Promise<void> {
const validCodes = shortCodes.filter((code) => this.isLikelyShortCode(code))
await Promise.allSettled(validCodes.map((code) => this.resolve(code)))
if (import.meta.env.DEV) {
console.log(`📦 预加载了 ${validCodes.length} 个短码`)
}
}
/**
* 从 URL 中提取可能的短码
*/
extractFromUrl(url: string): string[] {
const codes: string[] = []
// 提取路径参数
const pathParts = url.split('?')[0].split('/').filter(Boolean)
pathParts.forEach((part) => {
if (this.isLikelyShortCode(part)) {
codes.push(part)
}
})
// 提取查询参数
const queryString = url.split('?')[1]
if (queryString) {
const params = new URLSearchParams(queryString)
params.forEach((value) => {
if (this.isLikelyShortCode(value)) {
codes.push(value)
}
})
}
return codes
}
/**
* 清除缓存
*/
clearCache(shortCode?: string): void {
if (shortCode) {
this.cache.delete(shortCode)
} else {
this.cache.clear()
}
}
/**
* 获取缓存统计信息
*/
getCacheStats() {
return {
size: this.cache.size,
codes: Array.from(this.cache.keys())
}
}
}
// 导出单例
export const shortCodeResolver = new ShortCodeResolver()
// 开发环境下暴露调试接口
if (import.meta.env.DEV) {
;(window as any).__shortCodeResolver = {
resolve: (code: string) => shortCodeResolver.resolve(code),
clearCache: () => shortCodeResolver.clearCache(),
stats: () => shortCodeResolver.getCacheStats()
}
console.log('📦 短码解析器就绪 - 使用 window.__shortCodeResolver 调试')
}
<template>
<div>
<h1>产品列表</h1>
<div v-if="loading">加载中...</div>
<div v-else>
<pre>{{ query }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouteParams } from '@/composables/useRouteParams'
const query = ref<any>({})
const { ready, loading, params } = useRouteParams()
onMounted(async()=>{
await ready
query.value = params.value.city
})
</script>
......@@ -3,6 +3,10 @@
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Linting */
"strict": true,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment