明树Git Lab
Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
J
jt_front
Project
Project
Details
Activity
Releases
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
Administrator
jt_front
Commits
8c8b376a
Commit
8c8b376a
authored
May 21, 2026
by
zhanghan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
代码修改完成
parent
99d1fb18
Pipeline
#111665
passed with stage
in 19 seconds
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
716 additions
and
950 deletions
+716
-950
routes.js
src/router/routes.js
+1
-1
message.vue
src/views/systemManage/message.vue
+715
-201
newMessage.vue
src/views/systemManage/newMessage.vue
+0
-748
No files found.
src/router/routes.js
View file @
8c8b376a
...
@@ -26,7 +26,7 @@ const routes = [
...
@@ -26,7 +26,7 @@ const routes = [
name
:
"message"
,
name
:
"message"
,
title
:
"消息中心"
,
title
:
"消息中心"
,
meta
:
{
title
:
"消息中心"
},
meta
:
{
title
:
"消息中心"
},
component
:
()
=>
import
(
"@/views/systemManage/
newM
essage.vue"
),
component
:
()
=>
import
(
"@/views/systemManage/
m
essage.vue"
),
},
},
{
{
path
:
"/homePage"
,
path
:
"/homePage"
,
...
...
src/views/systemManage/message.vue
View file @
8c8b376a
<
template
>
<
template
>
<div
class=
"manage-container"
>
<div
class=
"manage-container"
>
<div
class=
"manage-wrap"
>
<div
class=
"manage-wrap message-page"
>
<search-form
@
search=
"handleSearch"
/>
<!-- 分类卡片 -->
<div
class=
"message-cards"
v-loading=
"loading"
>
<div
<div
class=
"system-manage-container message-container"
v-for=
"card in categoryCards"
v-loading=
"loading"
:key=
"card.key"
>
class=
"message-card"
<div
class=
"system-manage-content manage-content"
>
:class=
"
{ 'is-active': activeKey === card.key }"
<common-table
:style="{ '--card-color': card.color, '--card-bg': card.bg }"
:autoHeight=
"true"
@click="activeKey = card.key"
:maxRows=
"10"
>
:rowHeight=
"40"
<div
class=
"card-icon-wrap"
>
:data=
"tableData"
<el-icon
:size=
"26"
><component
:is=
"card.icon"
/></el-icon>
:columns=
"tableColumns"
</div>
:total=
"total"
<div
class=
"card-info"
>
:current-page=
"currentPage"
<div
class=
"card-name"
>
{{
card
.
name
}}
</div>
:page-size=
"pageSize"
<div
class=
"card-nums"
>
title=
""
<span
class=
"card-total"
>
{{
card
.
total
}}
条
</span>
:index=
"true"
<span
class=
"card-divider"
>
/
</span>
:border=
"true"
<span
class=
"card-unread"
v-if=
"card.unread > 0"
@
size-change=
"handleSizeChange"
>
{{
card
.
unread
}}
条未读
</span
@
current-page-change=
"handleCurrentPageChange"
>
<template
#
operations=
"
{ row, index }">
<el-button
link
type=
"primary"
size=
"small"
@
click=
"handlePreview(row, index)"
>
查看
</el-button>
<el-button
link
type=
"danger"
size=
"small"
@
click=
"handleDelete(row, index)"
>
>
删除
<span
class=
"card-read"
v-else
>
全部已读
</span>
</el-button
>
</div
>
</
template
>
</div
>
<
/common-table
>
<
div
class=
"card-badge"
v-if=
"card.unread > 0"
>
{{
card
.
unread
}}
</div
>
</div>
</div>
<el-dialog
</div>
v-model=
"messageDialogVisible"
title=
""
<!-- 下方左右两栏 -->
width=
"500"
<div
class=
"message-panels"
>
align-center
<!-- 左侧:最近消息时间线 -->
@
close=
"closeDialog"
<div
class=
"panel panel-recent"
>
>
<div
class=
"panel-header"
>
<div
class=
"message-wrap"
>
<span
class=
"panel-title"
>
最近消息
</span>
<div
class=
"message-title"
>
{{ messageInfo.title }}
</div>
</div>
<div
class=
"message-content"
>
<div
class=
"panel-body"
>
<div
class=
"label"
>
消息类型:
</div>
<div
v-if=
"recentMessages.length === 0"
class=
"panel-empty"
>
<div
class=
"info"
>
暂无消息
{{
messageInfo.type === 2
? "项目初步审核"
: messageInfo.type === 3
? "项目终审"
: "系统消息"
}}
</div>
</div>
</div>
<div
class=
"message-content"
>
<div
<div
class=
"label"
>
时间:
</div>
v-for=
"(msg, idx) in recentMessages"
<div
class=
"info"
>
:key=
"msg.id"
{{
class=
"recent-item"
proxy
@
click=
"handlePreview(msg)"
.moment(messageInfo.createdAt)
>
.format("YYYY-MM-DD HH:mm:SS")
<div
class=
"recent-dot-line"
>
}}
<span
class=
"recent-line"
v-if=
"idx
<
recentMessages
.
length
-
1
"
></span>
</div>
<div
class=
"recent-content"
>
<div
class=
"recent-top"
>
<span
class=
"recent-title"
:class=
"
{ 'is-unread': !msg.isRead }"
>
{{
msg
.
title
}}
</span
>
<el-tag
:type=
"getTypeTagColor(msg.type)"
size=
"small"
effect=
"light"
disable-transitions
>
{{
getTypeName
(
msg
.
type
)
}}
</el-tag>
</div>
<div
class=
"recent-bottom"
>
<span
class=
"recent-time"
>
{{
relativeTime
(
msg
.
createdAt
)
}}
</span>
<el-tag
v-if=
"!msg.isRead"
type=
"danger"
size=
"small"
effect=
"plain"
disable-transitions
>
未读
</el-tag
>
</div>
</div>
</div>
</div>
</div>
<div
class=
"message-content"
>
<div
class=
"label"
>
备注:
</div>
<div
class=
"info"
>
{{ messageInfo.content }}
</div>
</div>
</div>
</div>
<
template
#
footer
>
</div>
<div
class=
"dialog-footer"
>
<el-button
@
click=
"closeDialog"
>
关闭
</el-button>
<!-- 右侧:分类列表 -->
<el-button
type=
"primary"
@
click=
"toProjectPage"
<div
class=
"panel panel-list"
>
>
去处理
</el-button
<div
class=
"panel-header"
>
<span
class=
"panel-title"
>
{{
currentCard
.
name
}}
</span>
<span
class=
"panel-count"
></span>
</div>
<div
class=
"panel-body panel-body-table"
>
<el-table
:data=
"paginatedMessages"
style=
"width: 100%"
:header-cell-style=
"
{
background: '#fafafa',
color: '#333',
fontWeight: 600,
}"
empty-text="暂无消息"
:row-class-name="tableRowClass"
@row-click="handlePreview"
class="message-table"
>
<el-table-column
label=
"标题"
min-width=
"280px"
show-overflow-tooltip
>
>
</div>
<template
#
default=
"
{ row }">
</
template
>
<div
class=
"msg-title-cell"
>
</el-dialog>
<span
class=
"unread-dot"
v-if=
"!row.isRead"
></span>
<span
class=
"read-dot"
v-else
></span>
<span
class=
"msg-title-text"
:class=
"
{ 'is-unread': !row.isRead }"
>
{{
row
.
title
}}
</span
>
</div>
</
template
>
</el-table-column>
<el-table-column
label=
"类型"
width=
"140"
align=
"center"
>
<
template
#
default=
"{ row }"
>
<el-tag
:type=
"getTypeTagColor(row.type)"
size=
"small"
effect=
"light"
disable-transitions
>
{{
getTypeName
(
row
.
type
)
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
label=
"状态"
width=
"80"
align=
"center"
>
<
template
#
default=
"{ row }"
>
<el-tag
:type=
"row.isRead ? 'info' : 'danger'"
size=
"small"
effect=
"plain"
disable-transitions
>
{{
row
.
isRead
?
"已读"
:
"未读"
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
label=
"时间"
width=
"220"
align=
"center"
>
<
template
#
default=
"{ row }"
>
{{
formatTime
(
row
.
createdAt
)
}}
</
template
>
</el-table-column>
<el-table-column
label=
"操作"
width=
"120"
align=
"center"
fixed=
"right"
>
<
template
#
default=
"{ row }"
>
<el-button
link
type=
"primary"
size=
"small"
@
click
.
stop=
"handlePreview(row)"
>
查看
</el-button
>
<el-button
link
type=
"primary"
size=
"small"
@
click
.
stop=
"handleDelete(row)"
>
删除
</el-button
>
</
template
>
</el-table-column>
</el-table>
</div>
<div
class=
"pagination-wrap"
v-if=
"filteredMessages.length > pageSize"
>
<el-pagination
v-model:current-page=
"currentPage"
v-model:page-size=
"pageSize"
:page-sizes=
"[10, 20, 50, 100]"
:total=
"filteredMessages.length"
layout=
"total, sizes, prev, pager, next"
background
small
@
current-change=
"handlePageChange"
@
size-change=
"handleSizeChange"
/>
</div>
</div>
</div>
</div>
<!-- 消息详情弹窗 -->
<el-dialog
v-model=
"detailDialogVisible"
width=
"560"
align-center
:close-on-click-modal=
"false"
@
close=
"closeDetail"
>
<
template
#
header
>
<div
class=
"msg-dialog-header"
>
<el-tag
:type=
"getTypeTagColor(messageInfo.type)"
size=
"small"
effect=
"light"
disable-transitions
>
{{
getTypeName
(
messageInfo
.
type
)
}}
</el-tag>
<span
class=
"msg-dialog-time"
>
{{
formatTime
(
messageInfo
.
createdAt
)
}}
</span>
</div>
</
template
>
<div
class=
"msg-dialog-body"
>
<h3
class=
"msg-dialog-title"
>
{{ messageInfo.title }}
</h3>
<div
class=
"msg-dialog-content"
>
{{ messageInfo.content }}
</div>
</div>
<
template
#
footer
>
<el-button
@
click=
"closeDetail"
>
关闭
</el-button>
<el-button
type=
"primary"
@
click=
"toProjectPage"
>
去处理
</el-button>
</
template
>
</el-dialog>
</div>
</div>
</div>
</div>
</template>
</template>
<
script
setup
>
<
script
setup
>
import
{
import
{
ref
,
onMounted
,
getCurrentInstance
,
computed
,
watch
}
from
"vue"
;
ref
,
reactive
,
onMounted
,
getCurrentInstance
,
computed
,
nextTick
,
}
from
"vue"
;
import
{
ElMessage
,
ElMessageBox
}
from
"element-plus"
;
import
{
ElMessage
,
ElMessageBox
}
from
"element-plus"
;
import
{
Message
,
Bell
,
Edit
,
Stamp
}
from
"@element-plus/icons-vue"
;
import
{
useRouter
}
from
"vue-router"
;
import
{
useRouter
}
from
"vue-router"
;
import
CommonTable
from
"@/components/common/commonTable.vue"
;
import
{
useMessageStore
}
from
"@/stores/message.js"
;
import
SearchForm
from
"@/components/common/SearchForm.vue"
;
const
handleSearch
=
(
formData
)
=>
{
currentPage
.
value
=
1
;
loadTableData
(
formData
);
};
const
router
=
useRouter
();
const
router
=
useRouter
();
const
{
proxy
}
=
getCurrentInstance
();
const
{
proxy
}
=
getCurrentInstance
();
const
messageStore
=
useMessageStore
();
const
loading
=
ref
(
false
);
const
loading
=
ref
(
false
);
// 表格数据
const
allMessages
=
ref
([]);
const
tableData
=
ref
([]);
const
activeKey
=
ref
(
"all"
);
const
total
=
ref
(
0
);
const
currentPage
=
ref
(
1
);
const
currentPage
=
ref
(
1
);
const
pageSize
=
ref
(
10
);
const
pageSize
=
ref
(
20
);
// 表格列配置
const
tableColumns
=
[
const
getTypeName
=
(
type
)
=>
{
{
if
(
type
===
2
)
return
"项目初步审核"
;
prop
:
"title"
,
if
(
type
===
3
)
return
"项目终审"
;
label
:
"标题"
,
return
"系统消息"
;
minWidth
:
100
,
};
},
{
const
getTypeTagColor
=
(
type
)
=>
{
prop
:
"type"
,
if
(
type
===
2
)
return
"warning"
;
label
:
"消息类型"
,
if
(
type
===
3
)
return
"danger"
;
width
:
120
,
return
"success"
;
align
:
"center"
,
};
formatter
:
(
data
)
=>
{
return
data
.
type
===
2
const
getTypeColor
=
(
type
)
=>
{
?
"项目初步审核"
if
(
type
===
2
)
return
"#e6a23c"
;
:
data
.
type
===
3
if
(
type
===
3
)
return
"#f56c6c"
;
?
"项目终审"
return
"#67c23a"
;
:
"系统消息"
;
};
const
formatTime
=
(
time
)
=>
{
if
(
!
time
)
return
""
;
return
proxy
.
moment
(
time
).
format
(
"YYYY-MM-DD HH:mm:ss"
);
};
const
relativeTime
=
(
time
)
=>
{
if
(
!
time
)
return
""
;
const
m
=
proxy
.
moment
(
time
);
const
now
=
proxy
.
moment
();
const
diffMin
=
now
.
diff
(
m
,
"minutes"
);
if
(
diffMin
<
1
)
return
"刚刚"
;
if
(
diffMin
<
60
)
return
diffMin
+
" 分钟前"
;
const
diffHour
=
now
.
diff
(
m
,
"hours"
);
if
(
diffHour
<
24
)
return
diffHour
+
" 小时前"
;
const
diffDay
=
now
.
diff
(
m
,
"days"
);
if
(
diffDay
<
7
)
return
diffDay
+
" 天前"
;
return
m
.
format
(
"MM-DD HH:mm"
);
};
// 分类定义
const
categoryCards
=
computed
(()
=>
{
const
msgs
=
allMessages
.
value
;
const
sys
=
msgs
.
filter
((
m
)
=>
m
.
type
!==
2
&&
m
.
type
!==
3
);
const
chubu
=
msgs
.
filter
((
m
)
=>
m
.
type
===
2
);
const
zhongshen
=
msgs
.
filter
((
m
)
=>
m
.
type
===
3
);
return
[
{
key
:
"all"
,
name
:
"全部消息"
,
icon
:
Message
,
color
:
"#409eff"
,
bg
:
"rgba(64,158,255,0.06)"
,
total
:
msgs
.
length
,
unread
:
msgs
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
{
key
:
"system"
,
name
:
"系统消息"
,
icon
:
Bell
,
color
:
"#67c23a"
,
bg
:
"rgba(103,194,58,0.06)"
,
total
:
sys
.
length
,
unread
:
sys
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
},
},
{
{
key
:
"chubu"
,
prop
:
"isRead
"
,
name
:
"项目初步审核
"
,
label
:
"状态"
,
icon
:
Edit
,
width
:
90
,
color
:
"#e6a23c"
,
align
:
"center
"
,
bg
:
"rgba(230,162,60,0.06)
"
,
formatter
:
(
data
)
=>
{
total
:
chubu
.
length
,
return
data
.
isRead
?
"已读"
:
"未读"
;
unread
:
chubu
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
},
},
{
{
key
:
"zhongshen"
,
prop
:
"createdAt
"
,
name
:
"项目终审
"
,
label
:
"时间"
,
icon
:
Stamp
,
width
:
180
,
color
:
"#f56c6c"
,
align
:
"center
"
,
bg
:
"rgba(245,108,108,0.06)
"
,
formatter
:
(
data
)
=>
{
total
:
zhongshen
.
length
,
return
proxy
.
moment
(
data
.
createdAt
).
format
(
"YYYY-MM-DD HH:mm:SS"
);
unread
:
zhongshen
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
},
},
];
{
});
prop
:
"operations"
,
label
:
"操作"
,
const
currentCard
=
computed
(()
=>
{
width
:
100
,
return
(
slot
:
"operations"
,
categoryCards
.
value
.
find
((
c
)
=>
c
.
key
===
activeKey
.
value
)
||
fixed
:
"right"
,
categoryCards
.
value
[
0
]
align
:
"center"
,
);
},
});
];
const
loadTableData
=
(
formData
=
{})
=>
{
const
filteredMessages
=
computed
(()
=>
{
const
msgs
=
allMessages
.
value
;
if
(
activeKey
.
value
===
"all"
)
return
msgs
;
if
(
activeKey
.
value
===
"system"
)
return
msgs
.
filter
((
m
)
=>
m
.
type
!==
2
&&
m
.
type
!==
3
);
if
(
activeKey
.
value
===
"chubu"
)
return
msgs
.
filter
((
m
)
=>
m
.
type
===
2
);
if
(
activeKey
.
value
===
"zhongshen"
)
return
msgs
.
filter
((
m
)
=>
m
.
type
===
3
);
return
msgs
;
});
const
paginatedMessages
=
computed
(()
=>
{
const
start
=
(
currentPage
.
value
-
1
)
*
pageSize
.
value
;
return
filteredMessages
.
value
.
slice
(
start
,
start
+
pageSize
.
value
);
});
const
handlePageChange
=
(
page
)
=>
{
currentPage
.
value
=
page
;
};
const
handleSizeChange
=
()
=>
{
currentPage
.
value
=
1
;
};
const
recentMessages
=
computed
(()
=>
{
return
[...
filteredMessages
.
value
]
.
sort
((
a
,
b
)
=>
new
Date
(
b
.
createdAt
)
-
new
Date
(
a
.
createdAt
))
.
slice
(
0
,
10
);
});
const
tableRowClass
=
({
row
})
=>
{
return
row
.
isRead
?
"row-read"
:
"row-unread"
;
};
// 数据加载
const
loadAllMessages
=
()
=>
{
loading
.
value
=
true
;
loading
.
value
=
true
;
proxy
.
$post
({
proxy
.
$post
({
url
:
"/api/message/getUserMessages"
,
url
:
"/api/message/getUserMessages"
,
data
:
{
data
:
{
page
:
1
,
pageSize
:
9999
},
page
:
currentPage
.
value
,
pageSize
:
pageSize
.
value
,
...
formData
,
},
callback
:
(
data
)
=>
{
callback
:
(
data
)
=>
{
tableData
.
value
=
data
.
rows
;
allMessages
.
value
=
data
.
rows
||
[];
total
.
value
=
data
.
count
;
loading
.
value
=
false
;
loading
.
value
=
false
;
},
},
error
:
(
err
)
=>
{
error
:
()
=>
{
loading
.
value
=
false
;
loading
.
value
=
false
;
ElMessage
.
error
(
"加载数据失败"
);
ElMessage
.
error
(
"加载数据失败"
);
},
},
});
});
};
};
const
handleSizeChange
=
(
size
)
=>
{
pageSize
.
value
=
size
;
watch
(
activeKey
,
()
=>
{
currentPage
.
value
=
1
;
currentPage
.
value
=
1
;
loadTableData
();
});
};
const
handleCurrentPageChange
=
(
page
)
=>
{
currentPage
.
value
=
page
;
loadTableData
();
};
onMounted
(()
=>
{
onMounted
(()
=>
{
load
TableData
();
load
AllMessages
();
});
});
// 获取信息详情
let
messageDialogVisible
=
ref
(
false
);
// 消息详情
let
messageInfo
=
ref
({});
const
detailDialogVisible
=
ref
(
false
);
const
handlePreview
=
(
item
)
=>
{
const
messageInfo
=
ref
({});
const
handlePreview
=
(
row
)
=>
{
proxy
.
$post
({
proxy
.
$post
({
url
:
"/api/message/getMessageInfo"
,
url
:
"/api/message/getMessageInfo"
,
data
:
{
data
:
{
id
:
row
.
id
},
id
:
item
.
id
,
},
callback
:
(
data
)
=>
{
callback
:
(
data
)
=>
{
message
DialogVisible
.
value
=
true
;
detail
DialogVisible
.
value
=
true
;
messageInfo
.
value
=
data
;
messageInfo
.
value
=
data
;
if
(
!
row
.
isRead
)
{
row
.
isRead
=
true
;
messageStore
.
fetchMessageCount
();
}
},
},
error
:
(
err
)
=>
{
error
:
()
=>
{
ElMessage
.
error
(
"加载数据失败"
);
ElMessage
.
error
(
"加载数据失败"
);
},
},
});
});
};
};
const
closeDialog
=
()
=>
{
const
closeDetail
=
()
=>
{
messageInfo
.
value
=
{};
messageInfo
.
value
=
{};
message
DialogVisible
.
value
=
false
;
detail
DialogVisible
.
value
=
false
;
};
};
// 跳转到对应项目
const
toProjectPage
=
()
=>
{
const
toProjectPage
=
()
=>
{
router
.
push
({
router
.
push
({
name
:
"addProject"
,
name
:
"addProject"
,
query
:
{
query
:
{
projectId
:
messageInfo
.
value
.
projectId
,
isPreview
:
true
},
projectId
:
messageInfo
.
value
.
projectId
,
isPreview
:
true
,
},
});
});
};
};
// 删除
const
handleDelete
=
(
row
)
=>
{
const
handleDelete
=
(
row
)
=>
{
ElMessageBox
.
confirm
(
`确定删除该条消息?`
,
"提示"
,
{
ElMessageBox
.
confirm
(
"确定删除该条消息?"
,
"提示"
,
{
confirmButtonText
:
"确定"
,
confirmButtonText
:
"确定"
,
cancelButtonText
:
"取消"
,
cancelButtonText
:
"取消"
,
type
:
"warning"
,
type
:
"warning"
,
...
@@ -241,38 +436,357 @@ const handleDelete = (row) => {
...
@@ -241,38 +436,357 @@ const handleDelete = (row) => {
proxy
.
$post
({
proxy
.
$post
({
url
:
"/api/message/deleteMessage"
,
url
:
"/api/message/deleteMessage"
,
data
:
{
id
:
row
.
id
},
data
:
{
id
:
row
.
id
},
callback
:
(
data
)
=>
{
callback
:
()
=>
{
ElMessage
.
success
(
"删除成功"
);
ElMessage
.
success
(
"删除成功"
);
loadTableData
();
loadAllMessages
();
messageStore
.
fetchMessageCount
();
},
},
error
:
(
err
)
=>
{
error
:
()
=>
{
ElMessage
.
error
(
"删除失败
:"
,
err
);
ElMessage
.
error
(
"删除失败
"
);
},
},
});
});
});
});
};
};
</
script
>
</
script
>
<
style
lang=
"less"
>
<
style
lang=
"less"
scoped
>
.message-wrap {
.message-page {
.message-title {
padding: 12px;
font-size: 16px;
overflow-y: auto;
margin-bottom: 18px;
}
.message-page-header {
margin-bottom: 16px;
}
.message-page-title {
font-size: 18px;
font-weight: 600;
color: #1d2129;
}
/* ========== 分类卡片 ========== */
.message-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 16px;
}
.message-card {
position: relative;
background: #fff;
border-radius: 8px;
padding: 18px 16px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
border: 2px solid transparent;
user-select: none;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&.is-active {
border-color: var(--card-color);
background: var(--card-bg);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
}
}
.card-icon-wrap {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: var(--card-color);
color: #fff;
flex-shrink: 0;
}
.card-info {
flex: 1;
min-width: 0;
}
.card-name {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 4px;
}
.card-nums {
font-size: 12px;
color: #86909c;
display: flex;
align-items: center;
gap: 4px;
}
.card-divider {
color: #dcdfe6;
}
.card-unread {
color: #f56c6c;
font-weight: 500;
}
.card-read {
color: #67c23a;
}
.card-badge {
position: absolute;
top: 8px;
right: 10px;
min-width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
padding: 0 5px;
border-radius: 9px;
background: #f56c6c;
color: #fff;
font-size: 11px;
font-weight: 500;
}
/* ========== 下方两栏 ========== */
.message-panels {
display: grid;
grid-template-columns: 340px 1fr;
gap: 14px;
flex: 1;
min-height: 0;
}
.panel {
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
min-height: 0;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px 12px;
border-bottom: 1px solid #f2f3f5;
flex-shrink: 0;
}
.panel-title {
font-size: 15px;
font-weight: 600;
color: #1d2129;
}
.panel-count {
font-size: 13px;
color: #86909c;
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 10px 18px 16px;
}
.panel-body-table {
padding: 0;
overflow-y: auto;
}
.panel-empty {
text-align: center;
color: #c0c4cc;
font-size: 14px;
padding: 48px 0;
}
/* 左侧时间线 */
.recent-item {
display: flex;
gap: 12px;
cursor: pointer;
&:hover .recent-title {
color: #409eff;
}
}
.recent-dot {
width: 10px;
height: 10px;
min-height: 10px;
border-radius: 50%;
background: var(--dot-color, #dcdfe6);
flex-shrink: 0;
opacity: 0.45;
&.dot-unread {
opacity: 1;
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.15);
}
}
.recent-line {
width: 1px;
flex: 1;
background: #e8e8e8;
margin: 4px 0;
}
.recent-content {
flex: 1;
min-width: 0;
padding-bottom: 12px;
border-bottom: 1px solid #f7f7f7;
}
.recent-item:last-child .recent-content {
border-bottom: none;
padding-bottom: 0;
}
.recent-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.recent-title {
flex: 1;
min-width: 0;
font-size: 13px;
color: #4e5969;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s;
&.is-unread {
font-weight: 600;
font-weight: 600;
margin: 10px 0
;
color: #1d2129
;
}
}
.message-content {
}
display: flex;
align-items: flex-start;
.recent-bottom {
margin-bottom: 10px;
display: flex;
.label {
align-items: center;
width: 80px;
gap: 8px;
text-align: justify;
}
text-align-last: justify;
}
.recent-time {
.info {
font-size: 12px;
flex: 1;
color: #c0c4cc;
width: 0;
}
}
/* 右侧表格 */
.message-table {
flex: 1;
}
:deep(.message-table) {
.el-table__row {
cursor: pointer;
transition: background 0.15s;
}
.row-unread td {
background: #f0f7ff !important;
}
.el-table__row:hover > td {
background: #ecf5ff !important;
}
}
}
}
.msg-title-cell {
display: flex;
align-items: center;
gap: 8px;
}
.unread-dot {
width: 8px;
height: 8px;
min-width: 8px;
border-radius: 50%;
background: #f56c6c;
}
.read-dot {
width: 8px;
height: 8px;
min-width: 8px;
border-radius: 50%;
background: #dcdfe6;
}
.msg-title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.is-unread {
font-weight: 600;
color: #1d2129;
}
/* ========== 分页 ========== */
.pagination-wrap {
display: flex;
justify-content: flex-end;
padding: 12px 16px;
border-top: 1px solid #f2f3f5;
flex-shrink: 0;
background: #fff;
border-radius: 0 0 8px 8px;
}
/* ========== 详情弹窗 ========== */
.msg-dialog-header {
display: flex;
align-items: center;
gap: 12px;
}
.msg-dialog-time {
font-size: 13px;
color: #86909c;
}
.msg-dialog-body {
padding: 4px 0 12px;
}
.msg-dialog-title {
font-size: 17px;
font-weight: 600;
color: #1d2129;
margin: 0 0 16px;
line-height: 1.5;
}
.msg-dialog-content {
font-size: 14px;
color: #4e5969;
line-height: 1.8;
background: #f7f8fa;
border-radius: 6px;
padding: 14px 16px;
}
</
style
>
</
style
>
src/views/systemManage/newMessage.vue
deleted
100644 → 0
View file @
99d1fb18
<
template
>
<div
class=
"manage-container"
>
<div
class=
"manage-wrap message-page"
>
<!-- 分类卡片 -->
<div
class=
"message-cards"
v-loading=
"loading"
>
<div
v-for=
"card in categoryCards"
:key=
"card.key"
class=
"message-card"
:class=
"
{ 'is-active': activeKey === card.key }"
:style="{ '--card-color': card.color, '--card-bg': card.bg }"
@click="activeKey = card.key"
>
<div
class=
"card-icon-wrap"
>
<el-icon
:size=
"26"
><component
:is=
"card.icon"
/></el-icon>
</div>
<div
class=
"card-info"
>
<div
class=
"card-name"
>
{{
card
.
name
}}
</div>
<div
class=
"card-nums"
>
<span
class=
"card-total"
>
{{
card
.
total
}}
条
</span>
<span
class=
"card-divider"
>
/
</span>
<span
class=
"card-unread"
v-if=
"card.unread > 0"
>
{{
card
.
unread
}}
条未读
</span
>
<span
class=
"card-read"
v-else
>
全部已读
</span>
</div>
</div>
<div
class=
"card-badge"
v-if=
"card.unread > 0"
>
{{
card
.
unread
}}
</div>
</div>
</div>
<!-- 下方左右两栏 -->
<div
class=
"message-panels"
>
<!-- 左侧:最近消息时间线 -->
<div
class=
"panel panel-recent"
>
<div
class=
"panel-header"
>
<span
class=
"panel-title"
>
最近消息
</span>
</div>
<div
class=
"panel-body"
>
<div
v-if=
"recentMessages.length === 0"
class=
"panel-empty"
>
暂无消息
</div>
<div
v-for=
"(msg, idx) in recentMessages"
:key=
"msg.id"
class=
"recent-item"
@
click=
"handlePreview(msg)"
>
<div
class=
"recent-dot-line"
>
<span
class=
"recent-line"
v-if=
"idx
<
recentMessages
.
length
-
1
"
></span>
</div>
<div
class=
"recent-content"
>
<div
class=
"recent-top"
>
<span
class=
"recent-title"
:class=
"
{ 'is-unread': !msg.isRead }"
>
{{
msg
.
title
}}
</span
>
<el-tag
:type=
"getTypeTagColor(msg.type)"
size=
"small"
effect=
"light"
disable-transitions
>
{{
getTypeName
(
msg
.
type
)
}}
</el-tag>
</div>
<div
class=
"recent-bottom"
>
<span
class=
"recent-time"
>
{{
relativeTime
(
msg
.
createdAt
)
}}
</span>
<el-tag
v-if=
"!msg.isRead"
type=
"danger"
size=
"small"
effect=
"plain"
disable-transitions
>
未读
</el-tag
>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧:分类列表 -->
<div
class=
"panel panel-list"
>
<div
class=
"panel-header"
>
<span
class=
"panel-title"
>
{{
currentCard
.
name
}}
</span>
<span
class=
"panel-count"
>
共
{{
filteredMessages
.
length
}}
条
</span>
</div>
<div
class=
"panel-body panel-body-table"
>
<el-table
:data=
"filteredMessages"
style=
"width: 100%"
:header-cell-style=
"
{
background: '#fafafa',
color: '#333',
fontWeight: 600,
}"
empty-text="暂无消息"
:row-class-name="tableRowClass"
@row-click="handlePreview"
class="message-table"
>
<el-table-column
label=
"标题"
min-width=
"280px"
show-overflow-tooltip
>
<template
#
default=
"
{ row }">
<div
class=
"msg-title-cell"
>
<span
class=
"unread-dot"
v-if=
"!row.isRead"
></span>
<span
class=
"read-dot"
v-else
></span>
<span
class=
"msg-title-text"
:class=
"
{ 'is-unread': !row.isRead }"
>
{{
row
.
title
}}
</span
>
</div>
</
template
>
</el-table-column>
<el-table-column
label=
"类型"
width=
"140"
align=
"center"
>
<
template
#
default=
"{ row }"
>
<el-tag
:type=
"getTypeTagColor(row.type)"
size=
"small"
effect=
"light"
disable-transitions
>
{{
getTypeName
(
row
.
type
)
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
label=
"状态"
width=
"80"
align=
"center"
>
<
template
#
default=
"{ row }"
>
<el-tag
:type=
"row.isRead ? 'info' : 'danger'"
size=
"small"
effect=
"plain"
disable-transitions
>
{{
row
.
isRead
?
"已读"
:
"未读"
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
label=
"时间"
width=
"220"
align=
"center"
>
<
template
#
default=
"{ row }"
>
{{
formatTime
(
row
.
createdAt
)
}}
</
template
>
</el-table-column>
<el-table-column
label=
"操作"
width=
"120"
align=
"center"
fixed=
"right"
>
<
template
#
default=
"{ row }"
>
<el-button
link
type=
"primary"
size=
"small"
@
click
.
stop=
"handlePreview(row)"
>
查看
</el-button
>
<el-button
link
type=
"primary"
size=
"small"
@
click
.
stop=
"handleDelete(row)"
>
删除
</el-button
>
</
template
>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 消息详情弹窗 -->
<el-dialog
v-model=
"detailDialogVisible"
width=
"560"
align-center
:close-on-click-modal=
"false"
@
close=
"closeDetail"
>
<
template
#
header
>
<div
class=
"msg-dialog-header"
>
<el-tag
:type=
"getTypeTagColor(messageInfo.type)"
size=
"small"
effect=
"light"
disable-transitions
>
{{
getTypeName
(
messageInfo
.
type
)
}}
</el-tag>
<span
class=
"msg-dialog-time"
>
{{
formatTime
(
messageInfo
.
createdAt
)
}}
</span>
</div>
</
template
>
<div
class=
"msg-dialog-body"
>
<h3
class=
"msg-dialog-title"
>
{{ messageInfo.title }}
</h3>
<div
class=
"msg-dialog-content"
>
{{ messageInfo.content }}
</div>
</div>
<
template
#
footer
>
<el-button
@
click=
"closeDetail"
>
关闭
</el-button>
<el-button
type=
"primary"
@
click=
"toProjectPage"
>
去处理
</el-button>
</
template
>
</el-dialog>
</div>
</div>
</template>
<
script
setup
>
import
{
ref
,
onMounted
,
getCurrentInstance
,
computed
}
from
"vue"
;
import
{
ElMessage
,
ElMessageBox
}
from
"element-plus"
;
import
{
Message
,
Bell
,
Edit
,
Stamp
}
from
"@element-plus/icons-vue"
;
import
{
useRouter
}
from
"vue-router"
;
import
{
useMessageStore
}
from
"@/stores/message.js"
;
const
router
=
useRouter
();
const
{
proxy
}
=
getCurrentInstance
();
const
messageStore
=
useMessageStore
();
const
loading
=
ref
(
false
);
const
allMessages
=
ref
([]);
const
activeKey
=
ref
(
"all"
);
const
getTypeName
=
(
type
)
=>
{
if
(
type
===
2
)
return
"项目初步审核"
;
if
(
type
===
3
)
return
"项目终审"
;
return
"系统消息"
;
};
const
getTypeTagColor
=
(
type
)
=>
{
if
(
type
===
2
)
return
"warning"
;
if
(
type
===
3
)
return
"danger"
;
return
"success"
;
};
const
getTypeColor
=
(
type
)
=>
{
if
(
type
===
2
)
return
"#e6a23c"
;
if
(
type
===
3
)
return
"#f56c6c"
;
return
"#67c23a"
;
};
const
formatTime
=
(
time
)
=>
{
if
(
!
time
)
return
""
;
return
proxy
.
moment
(
time
).
format
(
"YYYY-MM-DD HH:mm:ss"
);
};
const
relativeTime
=
(
time
)
=>
{
if
(
!
time
)
return
""
;
const
m
=
proxy
.
moment
(
time
);
const
now
=
proxy
.
moment
();
const
diffMin
=
now
.
diff
(
m
,
"minutes"
);
if
(
diffMin
<
1
)
return
"刚刚"
;
if
(
diffMin
<
60
)
return
diffMin
+
" 分钟前"
;
const
diffHour
=
now
.
diff
(
m
,
"hours"
);
if
(
diffHour
<
24
)
return
diffHour
+
" 小时前"
;
const
diffDay
=
now
.
diff
(
m
,
"days"
);
if
(
diffDay
<
7
)
return
diffDay
+
" 天前"
;
return
m
.
format
(
"MM-DD HH:mm"
);
};
// 分类定义
const
categoryCards
=
computed
(()
=>
{
const
msgs
=
allMessages
.
value
;
const
sys
=
msgs
.
filter
((
m
)
=>
m
.
type
!==
2
&&
m
.
type
!==
3
);
const
chubu
=
msgs
.
filter
((
m
)
=>
m
.
type
===
2
);
const
zhongshen
=
msgs
.
filter
((
m
)
=>
m
.
type
===
3
);
return
[
{
key
:
"all"
,
name
:
"全部消息"
,
icon
:
Message
,
color
:
"#409eff"
,
bg
:
"rgba(64,158,255,0.06)"
,
total
:
msgs
.
length
,
unread
:
msgs
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
{
key
:
"system"
,
name
:
"系统消息"
,
icon
:
Bell
,
color
:
"#67c23a"
,
bg
:
"rgba(103,194,58,0.06)"
,
total
:
sys
.
length
,
unread
:
sys
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
{
key
:
"chubu"
,
name
:
"项目初步审核"
,
icon
:
Edit
,
color
:
"#e6a23c"
,
bg
:
"rgba(230,162,60,0.06)"
,
total
:
chubu
.
length
,
unread
:
chubu
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
{
key
:
"zhongshen"
,
name
:
"项目终审"
,
icon
:
Stamp
,
color
:
"#f56c6c"
,
bg
:
"rgba(245,108,108,0.06)"
,
total
:
zhongshen
.
length
,
unread
:
zhongshen
.
filter
((
m
)
=>
!
m
.
isRead
).
length
,
},
];
});
const
currentCard
=
computed
(()
=>
{
return
(
categoryCards
.
value
.
find
((
c
)
=>
c
.
key
===
activeKey
.
value
)
||
categoryCards
.
value
[
0
]
);
});
const
filteredMessages
=
computed
(()
=>
{
const
msgs
=
allMessages
.
value
;
if
(
activeKey
.
value
===
"all"
)
return
msgs
;
if
(
activeKey
.
value
===
"system"
)
return
msgs
.
filter
((
m
)
=>
m
.
type
!==
2
&&
m
.
type
!==
3
);
if
(
activeKey
.
value
===
"chubu"
)
return
msgs
.
filter
((
m
)
=>
m
.
type
===
2
);
if
(
activeKey
.
value
===
"zhongshen"
)
return
msgs
.
filter
((
m
)
=>
m
.
type
===
3
);
return
msgs
;
});
const
recentMessages
=
computed
(()
=>
{
return
[...
filteredMessages
.
value
]
.
sort
((
a
,
b
)
=>
new
Date
(
b
.
createdAt
)
-
new
Date
(
a
.
createdAt
))
.
slice
(
0
,
10
);
});
const
tableRowClass
=
({
row
})
=>
{
return
row
.
isRead
?
"row-read"
:
"row-unread"
;
};
// 数据加载
const
loadAllMessages
=
()
=>
{
loading
.
value
=
true
;
proxy
.
$post
({
url
:
"/api/message/getUserMessages"
,
data
:
{
page
:
1
,
pageSize
:
9999
},
callback
:
(
data
)
=>
{
allMessages
.
value
=
data
.
rows
||
[];
loading
.
value
=
false
;
},
error
:
()
=>
{
loading
.
value
=
false
;
ElMessage
.
error
(
"加载数据失败"
);
},
});
};
onMounted
(()
=>
{
loadAllMessages
();
});
// 消息详情
const
detailDialogVisible
=
ref
(
false
);
const
messageInfo
=
ref
({});
const
handlePreview
=
(
row
)
=>
{
proxy
.
$post
({
url
:
"/api/message/getMessageInfo"
,
data
:
{
id
:
row
.
id
},
callback
:
(
data
)
=>
{
detailDialogVisible
.
value
=
true
;
messageInfo
.
value
=
data
;
if
(
!
row
.
isRead
)
{
row
.
isRead
=
true
;
messageStore
.
fetchMessageCount
();
}
},
error
:
()
=>
{
ElMessage
.
error
(
"加载数据失败"
);
},
});
};
const
closeDetail
=
()
=>
{
messageInfo
.
value
=
{};
detailDialogVisible
.
value
=
false
;
};
const
toProjectPage
=
()
=>
{
router
.
push
({
name
:
"addProject"
,
query
:
{
projectId
:
messageInfo
.
value
.
projectId
,
isPreview
:
true
},
});
};
const
handleDelete
=
(
row
)
=>
{
ElMessageBox
.
confirm
(
"确定删除该条消息?"
,
"提示"
,
{
confirmButtonText
:
"确定"
,
cancelButtonText
:
"取消"
,
type
:
"warning"
,
}).
then
(()
=>
{
proxy
.
$post
({
url
:
"/api/message/deleteMessage"
,
data
:
{
id
:
row
.
id
},
callback
:
()
=>
{
ElMessage
.
success
(
"删除成功"
);
loadAllMessages
();
messageStore
.
fetchMessageCount
();
},
error
:
()
=>
{
ElMessage
.
error
(
"删除失败"
);
},
});
});
};
</
script
>
<
style
lang=
"less"
scoped
>
.message-page {
padding: 12px;
overflow-y: auto;
margin-bottom: 18px;
}
.message-page-header {
margin-bottom: 16px;
}
.message-page-title {
font-size: 18px;
font-weight: 600;
color: #1d2129;
}
/* ========== 分类卡片 ========== */
.message-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 16px;
}
.message-card {
position: relative;
background: #fff;
border-radius: 8px;
padding: 18px 16px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
border: 2px solid transparent;
user-select: none;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&.is-active {
border-color: var(--card-color);
background: var(--card-bg);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
}
}
.card-icon-wrap {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: var(--card-color);
color: #fff;
flex-shrink: 0;
}
.card-info {
flex: 1;
min-width: 0;
}
.card-name {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 4px;
}
.card-nums {
font-size: 12px;
color: #86909c;
display: flex;
align-items: center;
gap: 4px;
}
.card-divider {
color: #dcdfe6;
}
.card-unread {
color: #f56c6c;
font-weight: 500;
}
.card-read {
color: #67c23a;
}
.card-badge {
position: absolute;
top: 8px;
right: 10px;
min-width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
padding: 0 5px;
border-radius: 9px;
background: #f56c6c;
color: #fff;
font-size: 11px;
font-weight: 500;
}
/* ========== 下方两栏 ========== */
.message-panels {
display: grid;
grid-template-columns: 340px 1fr;
gap: 14px;
flex: 1;
min-height: 0;
}
.panel {
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
min-height: 0;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px 12px;
border-bottom: 1px solid #f2f3f5;
flex-shrink: 0;
}
.panel-title {
font-size: 15px;
font-weight: 600;
color: #1d2129;
}
.panel-count {
font-size: 13px;
color: #86909c;
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 10px 18px 16px;
}
.panel-body-table {
padding: 0;
}
.panel-empty {
text-align: center;
color: #c0c4cc;
font-size: 14px;
padding: 48px 0;
}
/* 左侧时间线 */
.recent-item {
display: flex;
gap: 12px;
cursor: pointer;
&:hover .recent-title {
color: #409eff;
}
}
.recent-dot {
width: 10px;
height: 10px;
min-height: 10px;
border-radius: 50%;
background: var(--dot-color, #dcdfe6);
flex-shrink: 0;
opacity: 0.45;
&.dot-unread {
opacity: 1;
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.15);
}
}
.recent-line {
width: 1px;
flex: 1;
background: #e8e8e8;
margin: 4px 0;
}
.recent-content {
flex: 1;
min-width: 0;
padding-bottom: 12px;
border-bottom: 1px solid #f7f7f7;
}
.recent-item:last-child .recent-content {
border-bottom: none;
padding-bottom: 0;
}
.recent-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.recent-title {
flex: 1;
min-width: 0;
font-size: 13px;
color: #4e5969;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s;
&.is-unread {
font-weight: 600;
color: #1d2129;
}
}
.recent-bottom {
display: flex;
align-items: center;
gap: 8px;
}
.recent-time {
font-size: 12px;
color: #c0c4cc;
}
/* 右侧表格 */
.message-table {
flex: 1;
}
:deep(.message-table) {
.el-table__row {
cursor: pointer;
transition: background 0.15s;
}
.row-unread td {
background: #f0f7ff !important;
}
.el-table__row:hover > td {
background: #ecf5ff !important;
}
}
.msg-title-cell {
display: flex;
align-items: center;
gap: 8px;
}
.unread-dot {
width: 8px;
height: 8px;
min-width: 8px;
border-radius: 50%;
background: #f56c6c;
}
.read-dot {
width: 8px;
height: 8px;
min-width: 8px;
border-radius: 50%;
background: #dcdfe6;
}
.msg-title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.is-unread {
font-weight: 600;
color: #1d2129;
}
/* ========== 详情弹窗 ========== */
.msg-dialog-header {
display: flex;
align-items: center;
gap: 12px;
}
.msg-dialog-time {
font-size: 13px;
color: #86909c;
}
.msg-dialog-body {
padding: 4px 0 12px;
}
.msg-dialog-title {
font-size: 17px;
font-weight: 600;
color: #1d2129;
margin: 0 0 16px;
line-height: 1.5;
}
.msg-dialog-content {
font-size: 14px;
color: #4e5969;
line-height: 1.8;
background: #f7f8fa;
border-radius: 6px;
padding: 14px 16px;
}
</
style
>
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