Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
corporate-culture-qd
Overview
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
王立鹏
corporate-culture-qd
Commits
545623b1
Commit
545623b1
authored
Apr 13, 2026
by
lijiabin
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '配置测试环境' into feature/21402-【YAYA文化岛】优化点整理
parents
b02715a9
98efd42c
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
557 additions
and
144 deletions
+557
-144
deploytest.js
deploy/deploytest.js
+532
-141
util.js
deploy/util.js
+19
-0
config.ts
src/config.ts
+1
-0
vite.config.ts
vite.config.ts
+5
-3
No files found.
deploy/deploytest.js
View file @
545623b1
import
cp
from
'node:child_process'
import
ssh
from
'ssh2'
import
archiver
from
'archiver'
import
fs
from
'node:fs'
import
path
from
'node:path'
import
cp
from
"node:child_process"
;
import
{
createHash
}
from
"node:crypto"
;
import
ssh
from
"ssh2"
;
import
archiver
from
"archiver"
;
import
fs
from
"node:fs"
;
import
path
from
"node:path"
;
import
{
getCurrentBranchName
,
getDeploySlug
}
from
"./util.js"
;
// 当前分支名
const
branchName
=
getCurrentBranchName
();
// 服务器路径名
const
deploySlug
=
getDeploySlug
(
branchName
);
// 最多部署的分支个数
const
MAX_DEPLOY_BRANCH_COUNT
=
5
;
const
connectInfo
=
{
host
:
"47.119.149.50"
,
port
:
"22"
,
username
:
"root"
,
password
:
"Qdt20250205"
,
};
const
remoteRootPath
=
"/usr/local/nginx/www/culture"
;
const
remoteBranchPath
=
`
${
remoteRootPath
}
/
${
deploySlug
}
`
;
const
remoteTarPath
=
`
${
remoteBranchPath
}
/dist.tar.gz`
;
const
remoteMetaPath
=
`
${
remoteBranchPath
}
/.deploy-meta.json`
;
const
unzipDirMode
=
{
spawn
:
'npm run build:test'
,
// 解压文件名
unzipDir
:
'culture/'
,
// 终端定位位置 cd
servicePath
:
'/usr/local/nginx/'
,
// 压缩包存放位置
serviceFilePath
:
'/usr/local/nginx/dist.tar.gz'
,
spawn
:
"npm run build:test"
,
servicePath
:
remoteRootPath
,
unzipDir
:
deploySlug
,
serviceFilePath
:
remoteTarPath
,
};
const
__dirname
=
path
.
resolve
();
const
distPath
=
path
.
resolve
(
__dirname
,
"dist"
);
const
zipPath
=
path
.
resolve
(
__dirname
,
"dist.tar.gz"
);
const
{
spawn
,
servicePath
,
unzipDir
,
serviceFilePath
}
=
unzipDirMode
;
let
conn
=
null
;
function
connectServer
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
conn
=
new
ssh
.
Client
();
conn
.
on
(
"ready"
,
()
=>
{
console
.
log
(
"SSH 连接成功"
);
resolve
(
conn
);
})
.
on
(
"error"
,
(
err
)
=>
{
reject
(
err
);
})
.
connect
(
connectInfo
);
});
}
const
__dirname
=
path
.
resolve
()
// 文件所在地
const
distPath
=
path
.
resolve
(
__dirname
,
'dist'
)
// 打包后位置
const
zipPath
=
path
.
resolve
(
__dirname
,
'dist.tar.gz'
)
// 远程服务器存放位置
const
{
spawn
,
servicePath
,
serviceFilePath
,
unzipDir
}
=
unzipDirMode
// 服务器连接信息
const
connectInfo
=
{
host
:
'47.119.149.50'
,
port
:
'22'
,
username
:
'root'
,
password
:
'Qdt20250205'
,
function
execCommand
(
command
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
conn
.
exec
(
command
,
{
encoding
:
"utf-8"
},
(
err
,
stream
)
=>
{
if
(
err
)
{
reject
(
err
);
return
;
}
let
stdout
=
""
;
let
stderr
=
""
;
stream
.
on
(
"data"
,
(
data
)
=>
{
stdout
+=
data
.
toString
();
});
stream
.
stderr
.
on
(
"data"
,
(
data
)
=>
{
stderr
+=
data
.
toString
();
});
stream
.
on
(
"close"
,
(
code
)
=>
{
if
(
code
===
0
)
{
resolve
(
stdout
);
return
;
}
reject
(
new
Error
(
stderr
.
trim
()
||
`命令执行失败:
${
command
}
`
));
});
});
});
}
// 获取远程服务器已部署的分支文件夹列表
async
function
getFolderList
()
{
const
result
=
await
execCommand
(
`find
${
remoteRootPath
}
-mindepth 1 -maxdepth 1 -type d -printf "%f\\n"`
,
);
return
result
.
split
(
"
\
n"
)
.
map
((
item
)
=>
item
.
trim
())
.
filter
(
Boolean
);
}
// 获取已部署的分支的元信息 中文名 服务器路径名 部署时间
async
function
getDeployEntries
(
folderList
)
{
const
entries
=
await
Promise
.
all
(
folderList
.
map
(
async
(
slug
)
=>
{
const
metaFilePath
=
`
${
remoteRootPath
}
/
${
slug
}
/.deploy-meta.json`
;
try
{
const
rawMeta
=
await
execCommand
(
`cat '
${
metaFilePath
}
'`
);
const
meta
=
JSON
.
parse
(
rawMeta
);
return
{
slug
,
branchName
:
meta
.
branchName
||
slug
,
deployedAt
:
meta
.
deployedAt
||
""
,
};
}
catch
{
return
{
slug
,
branchName
:
slug
,
deployedAt
:
""
,
};
}
}),
);
return
entries
.
sort
((
a
,
b
)
=>
a
.
branchName
.
localeCompare
(
b
.
branchName
,
"zh-CN"
));
}
// 写入index.html文件
async
function
writeIndexHtml
(
entries
)
{
// 剔除掉已经部署的
const
dedupedEntries
=
[];
const
seenSlugs
=
new
Set
();
for
(
const
entry
of
[{
slug
:
deploySlug
,
branchName
},
...
entries
])
{
if
(
seenSlugs
.
has
(
entry
.
slug
))
{
continue
;
}
seenSlugs
.
add
(
entry
.
slug
);
dedupedEntries
.
push
(
entry
);
}
const
indexHtml
=
`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>culture</title>
<style>
:root {
--bg: linear-gradient(135deg, #f4fbf8 0%, #e9f6ff 100%);
--card-bg: rgba(255, 255, 255, 0.88);
--card-border: rgba(137, 196, 181, 0.24);
--text-main: #16332d;
--text-sub: #5e7b74;
--button-bg: linear-gradient(135deg, #8be2c8 0%, #63d4c5 100%);
--button-shadow: 0 10px 24px rgba(99, 212, 197, 0.22);
--button-shadow-hover: 0 14px 28px rgba(99, 212, 197, 0.3);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text-main);
}
.page {
width: min(960px, calc(100vw - 32px));
margin: 0 auto;
padding: 40px 0 56px;
}
.hero {
margin-bottom: 24px;
padding: 28px 28px 24px;
border: 1px solid var(--card-border);
border-radius: 24px;
background: var(--card-bg);
backdrop-filter: blur(10px);
box-shadow: 0 20px 50px rgba(87, 143, 130, 0.12);
}
.hero h1 {
margin: 0 0 10px;
font-size: 28px;
line-height: 1.2;
}
.hero p {
margin: 0;
color: var(--text-sub);
font-size: 14px;
line-height: 1.7;
}
.account-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid rgba(137, 196, 181, 0.18);
}
.account-label {
font-size: 14px;
font-weight: 600;
color: var(--text-main);
}
.account-input {
flex: 1 1 260px;
min-width: 220px;
height: 44px;
padding: 0 14px;
border: 1px solid rgba(108, 180, 163, 0.28);
border-radius: 14px;
background: rgba(255, 255, 255, 0.92);
color: var(--text-main);
font-size: 14px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.account-input:focus {
border-color: rgba(74, 180, 152, 0.8);
box-shadow: 0 0 0 4px rgba(116, 211, 187, 0.14);
}
.account-tip {
width: 100%;
margin: 0;
color: var(--text-sub);
font-size: 12px;
line-height: 1.6;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.branch-card {
padding: 18px;
border: 1px solid rgba(137, 196, 181, 0.2);
border-radius: 20px;
background: rgba(255, 255, 255, 0.82);
box-shadow: 0 14px 30px rgba(126, 178, 167, 0.1);
}
.branch-name {
margin: 0 0 12px;
font-size: 16px;
font-weight: 600;
line-height: 1.5;
word-break: break-word;
}
.branch-slug {
margin: 0 0 16px;
color: var(--text-sub);
font-size: 12px;
line-height: 1.6;
word-break: break-all;
}
.branch-button {
width: 100%;
border: 0;
border-radius: 14px;
padding: 12px 16px;
background: var(--button-bg);
color: #13312a;
font-size: 14px;
font-weight: 600;
cursor: pointer;
box-shadow: var(--button-shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
}
.branch-button:hover {
transform: translateY(-2px);
box-shadow: var(--button-shadow-hover);
filter: saturate(1.05);
}
.branch-button:active {
transform: translateY(0);
}
@media (max-width: 640px) {
.page {
width: min(100vw - 20px, 960px);
padding-top: 20px;
}
.hero {
padding: 22px 18px 18px;
border-radius: 18px;
}
.hero h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<h1>Culture 分支导航</h1>
<p>这里展示当前可访问的测试分支,点击卡片按钮即可进入对应环境。</p>
<div class="account-panel">
<label class="account-label" for="accountInput">当前账号</label>
<input
id="accountInput"
class="account-input"
type="text"
placeholder="请输入账号"
/>
<p class="account-tip">账号会自动保存在浏览器本地,后续点击任意分支都会带上当前输入的 code 参数。</p>
</div>
</section>
<div class="grid">
${
dedupedEntries
.
map
(
(
item
)
=>
`
<section class="branch-card">
<h2 class="branch-name">分支「
${
item
.
branchName
}
」</h2>
<p class="branch-slug">访问路径:/
${
item
.
slug
}
/</p>
<button class="branch-button" onclick="goToBranch('
${
item
.
slug
}
')">
进入当前分支
</button>
</section>`
,
)
.
join
(
"
\
n"
)}
</div>
</div>
<script>
const STORAGE_KEY = 'culture_account_code'
const DEFAULT_ACCOUNT = 'lijiabin'
const accountInput = document.getElementById('accountInput')
function getAccountCode() {
const storedValue = window.localStorage.getItem(STORAGE_KEY)
return storedValue && storedValue.trim() ? storedValue.trim() : DEFAULT_ACCOUNT
}
function setAccountCode(value) {
const finalValue = value && value.trim() ? value.trim() : DEFAULT_ACCOUNT
window.localStorage.setItem(STORAGE_KEY, finalValue)
if (accountInput && accountInput.value !== finalValue) {
accountInput.value = finalValue
}
return finalValue
}
function goToBranch(slug) {
const code = encodeURIComponent(getAccountCode())
window.location.href = '/' + slug + '/?code=' + code
}
if (accountInput) {
accountInput.value = getAccountCode()
accountInput.addEventListener('input', (event) => {
setAccountCode(event.target.value)
})
accountInput.addEventListener('blur', (event) => {
setAccountCode(event.target.value)
})
}
setAccountCode(getAccountCode())
</script>
</body>
</html>
`
;
return
new
Promise
((
resolve
,
reject
)
=>
{
conn
.
sftp
((
err
,
sftp
)
=>
{
if
(
err
)
{
reject
(
new
Error
(
`SFTP Error:
${
err
.
message
}
`
));
return
;
}
const
writeStream
=
sftp
.
createWriteStream
(
`
${
remoteRootPath
}
/index.html`
,
{
encoding
:
"utf8"
,
});
writeStream
.
on
(
"error"
,
(
error
)
=>
{
sftp
.
end
();
reject
(
new
Error
(
`写入失败:
${
error
.
message
}
`
));
});
writeStream
.
on
(
"close"
,
()
=>
{
console
.
log
(
"index.html 写入成功"
);
sftp
.
end
();
resolve
();
});
writeStream
.
end
(
indexHtml
);
});
});
}
// 写入部署元信息
async
function
writeDeployMeta
()
{
const
meta
=
{
branchName
,
deploySlug
,
deployedAt
:
new
Date
().
toLocaleString
(
"zh-CN"
,
{
hour12
:
false
,
timeZone
:
"Asia/Shanghai"
,
}),
};
const
metaContent
=
JSON
.
stringify
(
meta
,
null
,
2
);
await
execCommand
(
`cat <<'EOF' > '
${
remoteMetaPath
}
'
${
metaContent
}
EOF`
);
}
//链接服务器
let
conn
=
new
ssh
.
Client
()
async
function
start
()
{
try
{
// 1. 打包
await
build
()
// 2. 压缩zip
await
startZip
()
// 3. 将zip文件传输至远程服务器
await
connect
()
// 4. 部署解压
await
shellCmd
(
conn
)
console
.
log
(
'部署完成'
)
await
connectServer
();
const
folderList
=
await
getFolderList
();
const
deployEntries
=
await
getDeployEntries
(
folderList
);
const
alreadyDeployed
=
deployEntries
.
some
((
item
)
=>
item
.
slug
===
deploySlug
);
if
(
!
alreadyDeployed
&&
deployEntries
.
length
>=
MAX_DEPLOY_BRANCH_COUNT
)
{
console
.
log
(
`当前已部署分支数量已达到上限
${
MAX_DEPLOY_BRANCH_COUNT
}
,请先手动删除以下分支中的一个或多个:`
,
);
deployEntries
.
forEach
((
item
,
index
)
=>
{
console
.
log
(
`
${
index
+
1
}
.
${
item
.
branchName
}
(
${
item
.
slug
}
)`
);
});
return
;
}
await
writeIndexHtml
(
deployEntries
);
await
build
();
await
startZip
();
await
uploadTar
();
await
shellCmd
();
await
writeDeployMeta
();
console
.
log
(
`部署完成,当前分支「
${
branchName
}
」对应目录:
${
deploySlug
}
`
);
}
catch
(
error
)
{
console
.
error
(
'Error:'
,
error
.
message
)
console
.
error
(
"Error:"
,
error
.
message
);
}
finally
{
// 5. 断开ssh,并删除本地压缩包
conn
.
end
()
delZip
()
if
(
conn
)
{
conn
.
end
();
}
delZip
();
}
}
start
()
/**
* 1. 本地构建项目
*/
start
();
function
build
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
//对项目进行打包,然后生成压缩文件
let
pro
=
cp
.
spawn
(
spawn
,
{
const
pro
=
cp
.
spawn
(
spawn
,
{
shell
:
true
,
stdio
:
'inherit'
,
})
pro
.
on
(
'exit'
,
(
code
)
=>
{
//打包完成后 开始链接目标服务器,并自动部署
stdio
:
"inherit"
,
})
;
pro
.
on
(
"exit"
,
(
code
)
=>
{
if
(
code
===
0
)
{
console
.
log
(
'---构建成功---'
)
resolve
()
}
else
{
reject
(
new
Error
(
'构建失败'
))
console
.
log
(
"---构建成功---"
);
resolve
();
return
;
}
})
})
reject
(
new
Error
(
"构建失败"
));
});
});
}
/**
* 2. 将打包后文件压缩zip
* @returns
*/
function
startZip
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
console
.
log
(
'开始打包tar'
)
//定义打包格式和相关配置
const
archive
=
archiver
(
'tar'
,
{
gzip
:
true
,
// 如果需要压缩,可以使用 gzip
gzipOptions
:
{
level
:
9
},
// gzip 压缩级别
}).
on
(
'error'
,
(
err
)
=>
reject
(
err
))
console
.
log
(
'zipPath'
,
zipPath
)
const
output
=
fs
.
createWriteStream
(
zipPath
)
//监听流的打包
output
.
on
(
'close'
,
(
err
)
=>
{
console
.
log
(
'err'
,
err
)
console
.
log
(
'目标打包完成'
)
resolve
(
true
)
})
//开始压缩
archive
.
pipe
(
output
)
// 文件夹压缩
archive
.
directory
(
distPath
,
false
)
archive
.
finalize
()
})
}
console
.
log
(
"开始打包 tar"
);
/**
* 3. 将zip文件传输至远程服务器
*/
function
connect
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
conn
.
on
(
'ready'
,
()
=>
{
conn
.
sftp
((
err
,
sftp
)
=>
{
if
(
err
)
{
return
reject
(
err
)
}
sftp
.
fastPut
(
zipPath
,
serviceFilePath
,
{},
(
err
)
=>
{
if
(
err
)
{
return
reject
(
err
)
}
//开始上传
console
.
log
(
'文件上传成功'
)
resolve
()
})
})
})
.
on
(
'error'
,
(
err
)
=>
reject
(
err
))
.
connect
(
connectInfo
)
})
const
archive
=
archiver
(
"tar"
,
{
gzip
:
true
,
gzipOptions
:
{
level
:
9
},
});
archive
.
on
(
"error"
,
(
err
)
=>
{
reject
(
err
);
});
console
.
log
(
"zipPath"
,
zipPath
);
const
output
=
fs
.
createWriteStream
(
zipPath
);
output
.
on
(
"close"
,
()
=>
{
console
.
log
(
"目标打包完成"
);
resolve
(
true
);
});
output
.
on
(
"error"
,
(
err
)
=>
{
reject
(
err
);
});
archive
.
pipe
(
output
);
archive
.
directory
(
distPath
,
false
);
archive
.
finalize
();
});
}
/**
* 4. 解压部署操作
* @param {*} conn
*/
async
function
shellCmd
(
conn
)
{
async
function
uploadTar
()
{
await
execCommand
(
`mkdir -p
${
servicePath
}
/
${
unzipDir
}
`
);
return
new
Promise
((
resolve
,
reject
)
=>
{
conn
.
s
hell
((
err
,
stream
)
=>
{
conn
.
s
ftp
((
err
,
sftp
)
=>
{
if
(
err
)
{
return
reject
(
err
)
reject
(
err
);
return
;
}
//进入服务器暂存地址
//解压上传的压缩包
//移动解压后的文件到发布目录
//删除压缩包
//退出
const
commands
=
`
cd
${
servicePath
}
&&
mkdir -p
${
unzipDir
}
&&
tar -xzf dist.tar.gz -C
${
unzipDir
}
&&
rm -rf dist.tar.gz &&
exit
`
console
.
log
(
'终端执行命令:'
,
commands
)
stream
.
on
(
'close'
,
()
=>
resolve
())
.
on
(
'data'
,
(
data
)
=>
console
.
log
(
data
.
toString
()))
.
stderr
.
on
(
'data'
,
(
data
)
=>
console
.
error
(
'Error:'
,
data
.
toString
()))
stream
.
end
(
commands
)
})
})
sftp
.
fastPut
(
zipPath
,
serviceFilePath
,
{},
(
putErr
)
=>
{
sftp
.
end
();
if
(
putErr
)
{
reject
(
putErr
);
return
;
}
console
.
log
(
"文件上传成功"
);
resolve
();
});
});
});
}
async
function
shellCmd
()
{
const
command
=
`cd
${
remoteBranchPath
}
&& tar -xzf dist.tar.gz && rm -f dist.tar.gz`
;
console
.
log
(
"终端执行命令:"
,
command
);
await
execCommand
(
command
);
}
/**
* 5. 删除本地的dist.zip
*/
function
delZip
()
{
fs
.
unlink
(
zipPath
,
function
(
err
)
{
if
(
!
fs
.
existsSync
(
zipPath
))
{
return
;
}
fs
.
unlink
(
zipPath
,
(
err
)
=>
{
if
(
err
)
{
console
.
error
(
'删除文件失败:'
,
err
.
message
)
console
.
error
(
"删除文件失败:"
,
err
.
message
);
return
;
}
console
.
log
(
'文件:'
+
zipPath
+
'删除成功!'
)
})
console
.
log
(
`文件:
${
zipPath
}
删除成功!`
);
});
}
deploy/util.js
0 → 100644
View file @
545623b1
import
{
execSync
}
from
'node:child_process'
import
{
createHash
}
from
'node:crypto'
// 获取当前分支名 去掉空格 斜杠 换行 等特殊字符
export
const
getCurrentBranchName
=
()
=>
{
const
branchName
=
execSync
(
'git branch --show-current'
,
{
encoding
:
'utf-8'
,
})
.
trim
()
.
replaceAll
(
' '
,
'_'
)
.
replaceAll
(
'/'
,
'_'
)
.
replaceAll
(
'
\
n'
,
'_'
)
return
branchName
}
// 根据分支名创建一个唯一的服务器文件夹名字
export
const
getDeploySlug
=
(
branchName
)
=>
{
return
`branch-
${
createHash
(
'md5'
).
update
(
branchName
).
digest
(
'hex'
).
slice
(
0
,
8
)}
`
}
src/config.ts
View file @
545623b1
...
...
@@ -17,6 +17,7 @@ export const app_config: { [key: string]: IConfig } = {
// 测试环境 暂时无测试环境部署
test
:
{
// baseUrl: 'http://culture.yswg.com.cn:8089', // 线上测试机
baseUrl
:
'http://192.168.2.55:8089'
,
// 首拥本地
loginType
:
1
,
wxRedirect
:
''
,
...
...
vite.config.ts
View file @
545623b1
...
...
@@ -15,10 +15,12 @@ import Icons from 'unplugin-icons/vite'
import
path
from
'node:path'
// 获取到执行脚本的--参数
import
pkg
from
'./package.json'
// @ts-ignore
import
{
getCurrentBranchName
,
getDeploySlug
}
from
'./deploy/util.js'
// https://vite.dev/config/
export
default
defineConfig
(({
mode
})
=>
{
return
{
base
:
mode
===
'test'
?
`/
${
getDeploySlug
(
getCurrentBranchName
())}
/`
:
'/'
,
define
:
{
__CORE_LIB_VERSION__
:
JSON
.
stringify
(
pkg
.
dependencies
),
},
...
...
@@ -62,7 +64,7 @@ export default defineConfig(({ mode }) => {
symbolId
:
'icon-[dir]-[name]'
,
}),
mode
===
'development'
&&
visualizer
(),
// 开发环境打包才需要
pushUpdatePlugin
(),
mode
===
'production'
&&
pushUpdatePlugin
(),
// 生产环境打包才更新推送
],
server
:
{
// 是否开启 https
...
...
@@ -80,7 +82,7 @@ export default defineConfig(({ mode }) => {
// },
},
build
:
{
minify
:
'esbuild'
,
minify
:
'esbuild'
,
// 'esbuild'
chunkSizeWarningLimit
:
1000
,
rollupOptions
:
{
output
:
{
...
...
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