feat:chat style

This commit is contained in:
lqins
2024-12-11 09:36:12 +08:00
parent 1b7c7a0dc1
commit 710b008453
44 changed files with 2274 additions and 1312 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -6,9 +6,13 @@ $borderColor = #4676d0;
.chat-page { .chat-page {
height: 100%; height: 100%;
::v-deep (.el-message-box__message){
font-size: 18px !important
}
// left side // left side
.el-container{
height: 100%;
}
.el-aside { .el-aside {
//background-color: $sideBgColor; //background-color: $sideBgColor;
padding 10px padding 10px
@@ -23,15 +27,16 @@ $borderColor = #4676d0;
.search-box { .search-box {
flex-wrap: wrap flex-wrap: wrap
padding: 10px 0; margin-bottom: 10px
// padding: 10px 0;
.search-input { // .search-input {
--el-input-bg-color: #363535 // --el-input-bg-color: #363535
--el-input-border-color: #464545 // --el-input-border-color: #464545
--el-input-focus-border-color: #47fff1 // --el-input-focus-border-color: #47fff1
--el-input-hover-border-color: #2DA39A // --el-input-hover-border-color: #2DA39A
box-shadow: none // box-shadow: none
} // }
} }
// //
@@ -53,12 +58,14 @@ $borderColor = #4676d0;
padding: 8px 12px padding: 8px 12px
//border-bottom: 1px solid #3c3c3c //border-bottom: 1px solid #3c3c3c
cursor: pointer cursor: pointer
border: 1px solid #3c3c3c // border: 1px solid #3c3c3c
margin-bottom 6px margin-bottom 6px
border-radius 5px border-radius 5px
&:hover { &:hover {
background-color #343540 // background-color :rgba(239, 241, 246, 0.64);
border: 1px solid var(--border-active);
} }
.avatar { .avatar {
@@ -78,7 +85,7 @@ $borderColor = #4676d0;
} }
.chat-title { .chat-title {
color: #c1c1c1 color: var(--el-text-color-regular);
padding: 5px 10px; padding: 5px 10px;
max-width 220px; max-width 220px;
font-size 14px; font-size 14px;
@@ -92,10 +99,11 @@ $borderColor = #4676d0;
position: absolute; position: absolute;
right: 2px; right: 2px;
top: 16px; top: 16px;
color #ffffff color var(--text-fb)
.el-dropdown-link { .el-dropdown-link {
color #ffffff color var(--text-fb)
} }
.el-icon { .el-icon {
@@ -105,8 +113,10 @@ $borderColor = #4676d0;
} }
.chat-list-item.active { .chat-list-item.active {
background-color: #343540; background-color :var(--theme-bg);
border-color #21aa93 box-shadow: 0px 3px 9px rgba(112,144,176,0.12);
border: 1px solid var(--border-active);
} }
} }
} }
@@ -116,7 +126,7 @@ $borderColor = #4676d0;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding-top 12px padding-top 12px
border-top 1px solid #3c3c3c; // border-top 0.5px solid var(--el-border-color);
.iconfont { .iconfont {
margin-right 5px margin-right 5px
@@ -134,14 +144,14 @@ $borderColor = #4676d0;
flex: 1; flex: 1;
background-color: var(--el-bg-color) background-color: var(--el-bg-color)
color var(--el-text-color-primary) color var(--el-text-color-primary)
.chat-config { .chat-config {
height 30px height 30px
padding 10px 30px padding 10px 30px
display flex display flex
justify-content center justify-content center
justify-items center justify-items center
border-bottom 1px solid #d9d9e3 // border-bottom 1px solid var(--el-border-color);
.role-select-label { .role-select-label {
color #ffffff color #ffffff
@@ -157,18 +167,22 @@ $borderColor = #4676d0;
} }
.setting { .setting {
padding 5px // padding 5px
border-radius 5px border-radius 5px
cursor pointer cursor pointer
background-color #f2f2f2 width: 26px;
margin-right 10px height: 26px;
text-align: center;
line-height: 26px;
// background-color #f2f2f2
// margin-right 10px
.iconfont { .iconfont {
font-size 18px font-size 16px
color #19c37d color var(--el-color-primary)
} }
&:hover { &:hover {
background-color #D5FAD3 background-color var(--text--hover)
} }
} }
@@ -183,7 +197,8 @@ $borderColor = #4676d0;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
position relative position relative
background: var(--chat-bg)
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px /* */ width: 12px /* */
background #F1F1F1 background #F1F1F1
@@ -217,6 +232,7 @@ $borderColor = #4676d0;
font-size: 14px; font-size: 14px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} }
} }
@@ -228,10 +244,11 @@ $borderColor = #4676d0;
.input-box-inner { .input-box-inner {
display flex display flex
background-color: #ffffff background-color:var(--chat-bg);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); // box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding 0 15px; padding 0 15px;
.tool-item { .tool-item {
@@ -243,7 +260,7 @@ $borderColor = #4676d0;
justify-items center justify-items center
padding 6px padding 6px
cursor pointer cursor pointer
background #F2F2F2 // background #F2F2F2
&:hover { &:hover {
background #D5FAD3 background #D5FAD3
@@ -274,13 +291,17 @@ $borderColor = #4676d0;
} }
.input-border { .input-border {
display flex // display flex
width 100% width 100%
overflow hidden overflow hidden
border: 2px solid #21AA93 border: 2px solid var( --theme-border-primary)
border-radius 10px border-radius 10px
padding 10px padding 10px
background-color #F4F4F4 // background-color #F4F4F4
&:hover{
border-color var(--theme-border-hover)
}
.input-inner { .input-inner {
display flex display flex
@@ -296,6 +317,7 @@ $borderColor = #4676d0;
} }
.prompt-input { .prompt-input {
min-height: 58px;
width 100% width 100%
line-height: 24px line-height: 24px
border none border none
@@ -312,12 +334,34 @@ $borderColor = #4676d0;
.send-btn { .send-btn {
width 32px width 32px
margin-left 10px margin-left 10px
.el-button { .el-button {
padding 8px 5px; padding 8px 5px;
border-radius 6px; border-radius 6px;
font-size 20px; font-size 20px;
} }
} }
.little-btns{
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
.tool-item-btn{
.iconfont{
font-size: 19px;
}
}
}
.add-new{
.el-icon{
font-size: 20px;
color: #754ff6;
}
cursor:pointer
}
} }
} }
@@ -380,7 +424,8 @@ $borderColor = #4676d0;
width 40px width 40px
height 40px height 40px
border-radius 100% border-radius 100%
background-color #ffffff background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
} }
} }
@@ -417,9 +462,13 @@ $borderColor = #4676d0;
line-height 1.8 line-height 1.8
font-size 16px font-size 16px
overflow auto overflow auto
height 100% height: 70vh
} }
} }
.dialog-footer{
margin-right: 22px;
}
} }
} }
@@ -436,4 +485,5 @@ $borderColor = #4676d0;
.el-icon { .el-icon {
margin-left 5px; margin-left 5px;
} }
} }

View File

@@ -2,16 +2,34 @@
--sm-txt:rgba(163, 174, 208, 1); --sm-txt:rgba(163, 174, 208, 1);
--text-secondary: #8a939d; --text-secondary: #8a939d;
--el-color-primary: rgb(107, 80, 225); --el-color-primary: rgb(107, 80, 225);
--el-component-size: 48px;
--el-border-radius-base: 8px; --el-border-radius-base: 8px;
--el-color-primary-light-5:rgb(107, 85, 255); --el-color-primary-light-5:rgb(107, 85, 255);
--el-color-primary-light-3:rgb(78, 51, 254); --el-color-primary-light-3:rgb(78, 51, 254);
--theme-btn-color:rgba(117, 81, 255, 1) --theme-btn-color:rgba(117, 81, 255, 1)
--common-text-color:#6e4ef9 --common-text-color:#6e4ef9;
--el-component-size: 36px;
--el-color-primary-dark-2:rgb(169 152 247);
--el-button-active-border-color:rgb(169 152 247);
--el-color-success-light-9:#EAFFFC;
--el-color-success-light-8:#A7F0D9;
--el-message-text-color:#0ECD8B;
--el-color-success:#0ECD8B;
--text-fff:#fff --text-fff:#fff
} --theme-border-primary: rgba(86, 86, 95, .322); //
--theme-border-hover: rgb(107, 85, 255);//hover
--text--hover:rgba(215, 211, 240, 0.581) //hover
}
.el-dialog{
--el-border-radius-base: 20px;
}
.login-box{
--el-component-size: 48px;
}
.btn-go{ .btn-go{
background: var(--btnColor); background: var(--btnColor);
color: #fff; color: #fff;
@@ -55,4 +73,46 @@
} }
.el-input__wrapper{ .el-input__wrapper{
background: var( --card-bg) background: var( --card-bg)
}
.el-dialog__title{
font-weight: bold;
line-height: 28px;
}
.el-button--primary{
border-radius: 8px;
}
/* */
::-webkit-scrollbar {
width: 12px; /* */
height: 12px; /* */
}
/* */
::-webkit-scrollbar-track {
background: #f1f1f1; /* */
border-radius: 6px; /* */
}
/* */
::-webkit-scrollbar-thumb {
background: #888; /* */
border-radius: 6px; /* */
}
/* */
::-webkit-scrollbar-thumb:hover {
background: #555; /* */
}
//.el-message-box
.el-message-box{
--el-messagebox-border-radius:18px
}
.el-message-box__container{
border-top: 1px solid #dbd3f4;
padding-top: 7px;
.el-message-box__message{
--text-color:var(--theme-text-color-primary)
font-size: 16px
}
} }

View File

@@ -1,166 +1,205 @@
.home { .layout{
display: flex; display: flex;
height 100vh position: relative;
width 100% height: 100vh;
flex-flow column .big-top-title{
padding-top: 10px;
.header { }
display flex .top-collapse{
justify-content space-between padding-top: 10px
height 50px img{
line-height 50px width 24px !important
background-color #1E1F22 height: 24px !important
padding-right 20px
.banner {
display flex
.logo {
display flex
padding 5px
cursor pointer
.el-image {
width 48px
height 48px
border-radius 50%
}
}
.title {
display: flex;
color: #ffffff;
font-size: 20px;
padding 0 10px
}
}
.navbar {
display flex
flex-flow row
.link-button {
margin-right 15px
color #e1e1e1
padding 0 10px
&:hover {
background-color #414141
}
.iconfont {
font-size 24px
}
}
.user-info {
width 100%
padding 5px 0;
.el-dropdown-link {
width 100%;
cursor: pointer
display flex
.el-image {
width: 36px;
height: 36px;
border-radius: 50%
}
.el-icon {
color: #cccccc;
line-height 24px;
}
}
}
} }
} }
.tab-box{
align-items: center
.main { background-color: var(--card-bg)
width 100% height: 100%
display flex .title{
flex-flow row font-size: 28px
font-weight: 700
.navigator { margin-right: 6px
display flex color:var(--text-theme-color)
flex-flow column
width 60px
padding 10px 1px
border-right: 1px solid #3c3c3c
background-color: #1E1F22
.nav-items {
margin-top: 10px;
padding 0 5px
li {
margin-bottom 15px
display flex
flex-flow column
a {
color #DADBDC
border-radius 10px
width 48px
height 48px
display flex
justify-content center
align-items center
cursor pointer
background-color #414348
.el-image {
border-radius 10px
}
.iconfont {
font-size 20px
}
}
a:hover, a.active {
color #47fff1
background-color #0F7A71
}
.title {
font-size: 12px
padding-top: 6px
color: #e5e7eb;
text-align: center;
white-space: nowrap; /* */
overflow: hidden; /* */
text-overflow: unset; /* 使 */
}
.active {
color #47fff1
}
}
}
} }
img{
.content { width 30px
width: 100% height: 30px
overflow auto object-fit: cover
box-sizing: border-box border-radius: 50%
background-color #282c34 padding: 4px
border: 2px solid #754ff6;
} }
.marr{
margin-right: 4px;
}
} }
} }
.flex-center-col{
display flex
align-items center
flex-direction column
}
.menu-list-collapse{
.flex-center-col{
flex-direction: row;
}
.menu-list-item{
height: 38px;
line-height: 38px;
}
.menu-list-item:hover,
.active{
background: rgba(79, 89, 102, .122);
border-radius: 8px;
.el-icon{
background: transparent !important;
}
}
.el-icon{
margin: 0 4px;
width 26px !important;
height: 26px !important;
}
.menu-title{
font-size: 15px !important;
margin-bottom: 0px !important;
}
}
.openicon{
font-size: 40px;
color: #754ff6;
}
.menuIcon{
.openicon{
font-size: 28px;
color: #754ff6;
}
}
.menu-list{
margin-top: 20px;
.svg-icon{
svg{
width: 30px;
height: 30px;
}
}
.menu-list-item{
// margin-bottom: 10px;
margin: 0 8px 8px;
cursor: pointer;
&:hover{
.el-icon{
background: rgba(79, 89, 102, .122);
}
}
.el-icon{
width: 24px;
height: 24px;
padding: 4px;
overflow: hidden;
border-radius: 50%;
font-size: 20px;
// img{
// width: 24px;
// height: 24px;
// }
}
&.active{
.el-icon{
background: rgba(79, 89, 102, .122);
}
}
}
.bot{
position: absolute;
bottom: 6px;
}
.bot-line{
width : 100%;
height: 1px;
background: var(--line-box)
margin: 20px 0 10px 0;
}
.menu-title{
font-size: 12px;
margin-bottom: 6px;
}
.icon-house,
.icon-github{
font-size: 20px;
color: #754ff6;
cursor pointer
}
.menu-bot-item{
display: flex;
align-items: center;
justify-content: space-around;
align-items: center;
a{
// margin-right: 46px;
}
}
}
::v-deep(.theme-box){
position: relative !important;
right: initial;
bottom: initial;
width: 20px;
height: 20px;
line-height: 20px;
.iconfont{
font-size: 15px !important;}
}
.right-main{
height: 100%;
// background: #f5f7fd;
background: var(--theme-bg-all);
// background-image: linear-gradient(180deg, rgba(247, 232, 255, .54), rgba(191, 223, 255, .35));
width: 100%;
.topheader{
display: flex;
position: fixed;
right: 8px;
z-index : 999;
top:0;
// width 100%;
align-items: center;
justify-content: flex-end;
}
.btn-go{
background: #754ff6;
margin: 10px 10px 0;
}
}
.el-popper { .el-popper {
.more-menus { .more-menus {
li { li {
padding 10px 15px padding: 0px 15px;
cursor pointer cursor: pointer;
border-radius 5px border-radius: 5px;
margin 5px 0 margin: 5px 0;
height: 38px;
line-height: 38px;
.el-image { .el-image {
position: relative position: relative
@@ -169,26 +208,49 @@
} }
&:hover { &:hover {
background-color #f1f1f1 background: rgba(79, 89, 102, 0.1);
} }
} }
li.active { li.active {
background-color #f1f1f1 background: rgba(79, 89, 102, 0.1);
} }
} }
.setting-menus{
.user-info-menu { .title{
li { color: #222226;
a {
width 100%
justify-content left
&:hover {
text-decoration none !important
color var(--el-primary-text-color)
}
}
} }
.el-icon,
.iconfont{
font-size: 18px
margin-right: 6px
}
color: #222226;
} }
.username{
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 1;
}
}
.rightHeightMax{
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
.rightHeight{
height: calc(100vh - 42px);
max-height: calc(100vh - 42px);
overflow: hidden;
.content{
padding-top: 42px;
}
}
.content{
height: 100%;
overflow: scroll;
} }

View File

@@ -199,3 +199,14 @@
transition: color 0.3s ease; /* */ transition: color 0.3s ease; /* */
font-weight: bold; font-weight: bold;
} }
.logo-box{
width: 60px
height: 60px
background: #fff
border-radius: 50%
img{
width: 100%;
object-fit: cover;
height: 100%;
}
}

View File

@@ -1,10 +1,12 @@
@import 'font.styl' @import 'font.styl'
:root[data-theme="dark"]{ :root[data-theme="dark"]{
--text-fb:#fff;
--text-color: rgba(255, 255, 255, 1) !important; // --text-color: rgba(255, 255, 255, 1) !important; //
--normal-color: rgba(163, 174, 208, 1); // --normal-color: rgba(163, 174, 208, 1); //
--el-text-color-primary: #fff;
p, h1, h2, h3, h4, h5, h6, article { p, h1, h2, h3, h4, h5, h6, article {
color: var(--text-color) !important; // color: var(--text-color) !important;
font-family: $font-regular; font-family: $font-regular;
} }
@@ -17,14 +19,42 @@
font-family: $font-regular; font-family: $font-regular;
} }
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active:rgba(255, 255, 255, 0.1);
--card-bg: rgba(17, 28, 68, 1); --card-bg: rgba(17, 28, 68, 1);
--theme-bg:rgb(13, 20, 53); --theme-bg:rgb(13, 20, 53);
--theme-bg-all:rgb(13, 20, 53);
--sign-bg: rgba(27, 37, 75, 1); --sign-bg: rgba(27, 37, 75, 1);
--text-theme-color: #fff; --text-theme-color: #fff;
--text-color-primary: #d1c7ff; --text-color-primary: #d1c7ff;
--el-text-color-regular: rgba(163, 174, 208, 1) --el-text-color-regular: rgba(163, 174, 208, 1)
--el-border-color:rgb(79, 80, 85) --el-border-color:rgb(79, 80, 85)
--el-text-color-primary: #fff; --el-text-color-primary: #fff;
--el-bg-color-overlay: rgba(17, 28, 68, 1);
--el-border-color-light: rgba(255, 255, 255, 0.2);
--line-box:rgba(255, 255, 255, 0.1);
--chat-bg:#141a36;
--el-bg-color:#141a36;
--el-fill-color-blank: rgba(17, 28, 68, 1);
--el-fill-color-light: rgba(86, 86, 95, .2);
--el-color-primary-light-9:rgba(86, 86, 95, .2);
--chat-wel-bg:#2d2f388a;
--theme-text-color-secondary: #a3aed0;
//layout
.more-menus li.moreTitle,
.twoTittle .title,
.setting-menus span.title,
.setting-menus li .el-icon,
.setting-menus li .iconfont,
.layout .tab-box .menu-list-item{
filter: invert(100%);
}
.more-menus span.title{
color:#000;
}
--theme-text-color-primary: #fff;
--theme-text-primary: #f3f3f3;
--chat-content-bg:rgba(86, 86, 95, .2);
--chat-content-bg-list:rgba(86, 86, 95, .2);
} }

View File

@@ -2,11 +2,12 @@
@import 'font.styl' @import 'font.styl'
:root[data-theme="light"] { :root[data-theme="light"] {
--text-fb:#000;
// rgba(43, 54, 116, 1) // rgba(43, 54, 116, 1)
--text-color: #5b62ce; // --text-color: #5b62ce; //
--normal-color: rgba(43, 54, 116, 1); // --normal-color: rgba(43, 54, 116, 1); //
p, h1, h2, h3, h4, h5, h6, article { p, h1, h2, h3, h4, h5, h6, article {
color: var(--text-color) !important; // color: var(--text-color) !important;
font-family: $font-regular; font-family: $font-regular;
} }
html, html,
@@ -22,12 +23,28 @@
}//#6b61f6 }//#6b61f6
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--border-active:rgba(134, 140, 255, 1);
--code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce); --code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
--card-bg:#fff; --card-bg:#fff;
--theme-bg:linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff); --theme-bg:linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff);
--theme-bg-all:#f5f7fd;
--sign-bg: rgba(244, 247, 254, 1); --sign-bg: rgba(244, 247, 254, 1);
--text-theme-color: rgba(43, 54, 116, 1) --text-theme-color: rgba(43, 54, 116, 1)
--text-color-primary: rgba(67, 24, 255, 1); --text-color-primary: rgba(67, 24, 255, 1);
--line-box:rgba(79, 89, 102, 0.122);
--el-bg-color-overlay: #fff;
--el-bg-color:#fff;
--el-fill-color-blank: #fff;
--theme-text-color-primary: #000;
--theme-text-primary: #000;
--theme-text-color-secondary: #666;
--chat-bg: #fff;
--chat-content-bg:#f5f7fc;
--chat-list-bg: #0302020a;
--chat-content-bg-list:#fff;
--chat-wel-bg:rgba(247, 247, 248, 1);
} }

View File

@@ -1,9 +1,10 @@
/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */
@font-face { @font-face {
font-family: 'iconfont'; /* Project id 4125778 */ font-family: 'iconfont'; /* Project id 4125778 */
src: url('//at.alicdn.com/t/c/font_4125778_gs96jfl3hlc.woff2?t=1732009095144') format('woff2'), src: url('//at.alicdn.com/t/c/font_4125778_t6hhjiqu67.woff2?t=1733221702650') format('woff2'),
url('//at.alicdn.com/t/c/font_4125778_gs96jfl3hlc.woff?t=1732009095144') format('woff'), url('//at.alicdn.com/t/c/font_4125778_t6hhjiqu67.woff?t=1733221702650') format('woff'),
url('//at.alicdn.com/t/c/font_4125778_gs96jfl3hlc.ttf?t=1732009095144') format('truetype'); url('//at.alicdn.com/t/c/font_4125778_t6hhjiqu67.ttf?t=1733221702650') format('truetype');
} }
.iconfont { .iconfont {
@@ -418,3 +419,6 @@
content: "\e66f"; content: "\e66f";
} }
.icon-house:before {
content: "\e619";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -56,5 +56,5 @@ import FooterBar from "@/components/FooterBar.vue";
object-fit: cover; object-fit: cover;
height: 100%; height: 100%;
} }
} }
</style> </style>

View File

@@ -1,19 +1,28 @@
<template> <template>
<button v-if="showButton" @click="scrollToTop" class="scroll-to-top" :style="{bottom: bottom + 'px', right: right + 'px', backgroundColor: bgColor}"> <button
v-if="showButton"
@click="scrollToTop"
class="scroll-to-top"
:style="{
bottom: bottom + 'px',
right: right + 'px',
backgroundColor: bgColor
}"
>
<el-icon><ArrowUpBold /></el-icon> <el-icon><ArrowUpBold /></el-icon>
</button> </button>
</template> </template>
<script> <script>
import {ArrowUpBold} from "@element-plus/icons-vue"; import { ArrowUpBold } from "@element-plus/icons-vue";
export default { export default {
name: 'BackTop', name: "BackTop",
components: {ArrowUpBold}, components: { ArrowUpBold },
props: { props: {
bottom: { bottom: {
type: Number, type: Number,
default: 30 default: 155
}, },
right: { right: {
type: Number, type: Number,
@@ -21,7 +30,7 @@ export default {
}, },
bgColor: { bgColor: {
type: String, type: String,
default: '#007bff' default: "#b6aaf9"
} }
}, },
data() { data() {
@@ -31,19 +40,19 @@ export default {
}, },
mounted() { mounted() {
this.checkScroll(); this.checkScroll();
window.addEventListener('resize', this.checkScroll); window.addEventListener("resize", this.checkScroll);
this.$el.parentElement.addEventListener('scroll', this.checkScroll); this.$el.parentElement.addEventListener("scroll", this.checkScroll);
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener('resize', this.checkScroll); window.removeEventListener("resize", this.checkScroll);
this.$el.parentElement.removeEventListener('scroll', this.checkScroll); this.$el.parentElement.removeEventListener("scroll", this.checkScroll);
}, },
methods: { methods: {
scrollToTop() { scrollToTop() {
const container = this.$el.parentElement; const container = this.$el.parentElement;
container.scrollTo({ container.scrollTo({
top: 0, top: 0,
behavior: 'smooth' behavior: "smooth"
}); });
}, },
checkScroll() { checkScroll() {
@@ -51,7 +60,7 @@ export default {
this.showButton = container.scrollTop > 50; this.showButton = container.scrollTop > 50;
} }
} }
} };
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
@@ -63,15 +72,15 @@ export default {
cursor: pointer; cursor: pointer;
outline: none; outline: none;
transition: opacity 0.3s; transition: opacity 0.3s;
width 40px width 30px
height 40px height 30px
display flex display flex
justify-content center justify-content center
align-items center align-items center
font-size 20px font-size 18px
&:hover { &:hover {
opacity: 0.6; opacity: 0.6;
} }
} }
</style> </style>

View File

@@ -1,65 +1,77 @@
<template> <template>
<div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'"> <div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
<div class="chat-line-inner"> <div class="chat-line-inner">
<div class="chat-icon"> <div class="chat-icon">
<img :src="data.icon" alt="User"/> <img :src="data.icon" alt="User" />
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div v-if="files.length > 0" class="file-list-box"> <div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files"> <div v-for="file in files">
<div class="image" v-if="isImage(file.ext)"> <div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/> <el-image :src="file.url" fit="cover" />
</div>
<div class="item" v-else>
<div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div> </div>
<div class="item" v-else> <div class="body">
<div class="icon"> <div class="title">
<el-image :src="GetFileIcon(file.ext)" fit="cover" /> <el-link
:href="file.url"
target="_blank"
style="--el-font-weight-primary: bold"
>{{ file.name }}</el-link
>
</div> </div>
<div class="body"> <div class="info">
<div class="title"> <span>{{ GetFileType(file.ext) }}</span>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link> <span>{{ FormatFileSize(file.size) }}</span>
</div>
<div class="info">
<span>{{GetFileType(file.ext)}}</span>
<span>{{FormatFileSize(file.size)}}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="content" v-html="content"></div> </div>
<div class="bar" v-if="data.created_at > 0"> <div class="content" v-html="content"></div>
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <div class="bar" v-if="data.created_at > 0">
<span class="bar-item">tokens: {{ finalTokens }}</span> <span class="bar-item"
</div> ><el-icon><Clock /></el-icon>
{{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ finalTokens }}</span>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="chat-line chat-line-prompt-chat" v-else> <div class="chat-line chat-line-prompt-chat" v-else>
<div class="chat-line-inner"> <div class="chat-line-inner">
<div class="chat-icon"> <div class="chat-icon">
<img :src="data.icon" alt="User"/> <img :src="data.icon" alt="User" />
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div v-if="files.length > 0" class="file-list-box"> <div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files"> <div v-for="file in files">
<div class="image" v-if="isImage(file.ext)"> <div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/> <el-image :src="file.url" fit="cover" />
</div> </div>
<div class="item" v-else> <div class="item" v-else>
<div class="icon"> <div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" /> <el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div> </div>
<div class="body"> <div class="body">
<div class="title"> <div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link> <el-link
:href="file.url"
target="_blank"
style="--el-font-weight-primary: bold"
>{{ file.name }}</el-link
>
</div> </div>
<div class="info"> <div class="info">
<span>{{GetFileType(file.ext)}}</span> <span>{{ GetFileType(file.ext) }}</span>
<span>{{FormatFileSize(file.size)}}</span> <span>{{ FormatFileSize(file.size) }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -69,8 +81,11 @@
<div class="content" v-html="content"></div> <div class="content" v-html="content"></div>
</div> </div>
<div class="bar" v-if="data.created_at > 0"> <div class="bar" v-if="data.created_at > 0">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>--> ><el-icon><Clock /></el-icon>
{{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
</div> </div>
</div> </div>
</div> </div>
@@ -78,104 +93,110 @@
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import { onMounted, ref } from "vue";
import {Clock} from "@element-plus/icons-vue"; import { Clock } from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http"; import { httpPost } from "@/utils/http";
import hl from "highlight.js"; import hl from "highlight.js";
import {dateFormat, isImage, processPrompt} from "@/utils/libs"; import { dateFormat, isImage, processPrompt } from "@/utils/libs";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system"; import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
const mathjaxPlugin = require('markdown-it-mathjax3') const mathjaxPlugin = require("markdown-it-mathjax3");
const md = require('markdown-it')({ const md = require("markdown-it")({
breaks: true, breaks: true,
html: true, html: true,
linkify: true, linkify: true,
typographer: true, typographer: true,
highlight: function (str, lang) { highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000) const codeIndex =
parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
// 显示复制代码按钮 // 显示复制代码按钮
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span> const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '&lt;/textarea>')}</textarea>` <textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
if (lang && hl.getLanguage(lang)) { if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>` const langHtml = `<span class="lang-name">${lang}</span>`;
// 处理代码高亮 // 处理代码高亮
const preCode = hl.highlight(lang, str, true).value const preCode = hl.highlight(lang, str, true).value;
// 将代码包裹在 pre 中 // 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>` return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
} }
// 处理代码高亮 // 处理代码高亮
const preCode = md.utils.escapeHtml(str) const preCode = md.utils.escapeHtml(str);
// 将代码包裹在 pre 中 // 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>` return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
} }
}); });
md.use(mathjaxPlugin) md.use(mathjaxPlugin);
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: { default: {
content: '', content: "",
created_at: '', created_at: "",
tokens: 0, tokens: 0,
model: '', model: "",
icon: '', icon: ""
}, }
}, },
listStyle: { listStyle: {
type: String, type: String,
default: 'list', default: "list"
}, }
}) });
const finalTokens = ref(props.data.tokens) const finalTokens = ref(props.data.tokens);
const content =ref(processPrompt(props.data.content)) const content = ref(processPrompt(props.data.content));
const files = ref([]) const files = ref([]);
onMounted(() => { onMounted(() => {
processFiles() processFiles();
}) });
const processFiles = () => { const processFiles = () => {
if (!props.data.content) { if (!props.data.content) {
return return;
} }
const linkRegex = /(https?:\/\/\S+)/g; const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex); const links = props.data.content.match(linkRegex);
if (links) { if (links) {
httpPost("/api/upload/list", {urls: links}).then(res => { httpPost("/api/upload/list", { urls: links })
files.value = res.data.items .then((res) => {
files.value = res.data.items;
for (let link of links) { for (let link of links) {
if (isExternalImg(link, files.value)) { if (isExternalImg(link, files.value)) {
files.value.push({url:link, ext: ".png"}) files.value.push({ url: link, ext: ".png" });
}
} }
} })
}).catch(() => { .catch(() => {});
})
for (let link of links) { for (let link of links) {
content.value = content.value.replace(link,"") content.value = content.value.replace(link, "");
} }
} }
content.value = md.render(content.value.trim()) content.value = md.render(content.value.trim());
} };
const isExternalImg = (link, files) => { const isExternalImg = (link, files) => {
return isImage(link) && !files.find(file => file.url === link) return isImage(link) && !files.find((file) => file.url === link);
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">
@import '@/assets/css/markdown/vue.css'; @import '@/assets/css/markdown/vue.css';
.chat-page,.chat-export { .chat-page,.chat-export {
.chat-line-prompt-list { .chat-line-prompt-list {
background-color #ffffff;
background-color:var( --chat-content-bg-list);
color:var(--theme-text-color-primary);
justify-content: center; justify-content: center;
width 100% width 100%
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
padding-top: 1.5rem; padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3; border-bottom: 0.5px solid var(--el-border-color);
.chat-line-inner { .chat-line-inner {
display flex; display flex;
@@ -189,7 +210,7 @@ const isExternalImg = (link, files) => {
img { img {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 10px; border-radius: 50%;
padding: 1px; padding: 1px;
} }
} }
@@ -218,8 +239,9 @@ const isExternalImg = (link, files) => {
display flex display flex
flex-flow row flex-flow row
border-radius 10px border-radius 10px
background-color #ffffff background-color:var(--chat-content-bg);
border 1px solid #e3e3e3 border 1px solid #e3e3e3
color:var(--theme-text-color-primary);
padding 6px padding 6px
margin-bottom 10px margin-bottom 10px
@@ -251,7 +273,7 @@ const isExternalImg = (link, files) => {
.content { .content {
word-break break-word; word-break break-word;
padding: 0; padding: 0;
color #374151; color:var(--theme-text-color-primary);
font-size: var(--content-font-size); font-size: var(--content-font-size);
border-radius: 5px; border-radius: 5px;
overflow: auto; overflow: auto;
@@ -279,7 +301,7 @@ const isExternalImg = (link, files) => {
padding 10px 10px 10px 0; padding 10px 10px 10px 0;
.bar-item { .bar-item {
background-color #f7f7f8; // background-color #f7f7f8;
color #888 color #888
padding 3px 5px; padding 3px 5px;
margin-right 10px; margin-right 10px;
@@ -298,7 +320,7 @@ const isExternalImg = (link, files) => {
} }
.chat-line-prompt-chat { .chat-line-prompt-chat {
background-color #ffffff; background: var(--chat-bg);
justify-content: center; justify-content: center;
width 100% width 100%
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
@@ -345,7 +367,8 @@ const isExternalImg = (link, files) => {
display flex display flex
flex-flow row flex-flow row
border-radius 10px border-radius 10px
background-color #ffffff background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
border 1px solid #e3e3e3 border 1px solid #e3e3e3
padding 6px padding 6px
margin-bottom 10px margin-bottom 10px
@@ -382,10 +405,10 @@ const isExternalImg = (link, files) => {
.content { .content {
word-break break-word; word-break break-word;
padding: 1rem padding: 1rem
color #222222; color var(--theme-text-primary);
font-size: var(--content-font-size); font-size: var(--content-font-size);
overflow: auto; overflow: auto;
background-color #98e165 background-color :var(--chat-content-bg);
border-radius: 10px 0 10px 10px; border-radius: 10px 0 10px 10px;
img { img {

View File

@@ -2,48 +2,54 @@
<div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'"> <div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
<div class="chat-line-inner"> <div class="chat-line-inner">
<div class="chat-icon"> <div class="chat-icon">
<img :src="data.icon" alt="ChatGPT"> <img :src="data.icon" alt="ChatGPT" />
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="content" v-html="md.render(processContent(data.content))"></div> <div
class="content"
v-html="md.render(processContent(data.content))"
></div>
<div class="bar" v-if="data.created_at"> <div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"
<span class="bar-item">tokens: {{ data.tokens }}</span> ><el-icon><Clock /></el-icon>
{{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ data.tokens }}</span>
<span class="bar-item"> <span class="bar-item">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="复制回答" content="复制回答"
placement="bottom" placement="bottom"
> >
<el-icon class="copy-reply" :data-clipboard-text="data.content"> <el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy/> <DocumentCopy />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</span> </span>
<span v-if="!readOnly"> <span v-if="!readOnly">
<span class="bar-item" @click="reGenerate(data.prompt)"> <span class="bar-item" @click="reGenerate(data.prompt)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="重新生成" content="重新生成"
placement="bottom" placement="bottom"
> >
<el-icon><Refresh/></el-icon> <el-icon><Refresh /></el-icon>
</el-tooltip> </el-tooltip>
</span> </span>
<span class="bar-item" @click="synthesis(data.content)"> <span class="bar-item" @click="synthesis(data.content)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="生成语音朗读" content="生成语音朗读"
placement="bottom" placement="bottom"
> >
<i class="iconfont icon-speaker"></i> <i class="iconfont icon-speaker"></i>
</el-tooltip> </el-tooltip>
</span> </span>
</span> </span>
<!-- <span class="bar-item">--> <!-- <span class="bar-item">-->
<!-- <el-dropdown trigger="click">--> <!-- <el-dropdown trigger="click">-->
@@ -65,49 +71,55 @@
<div class="chat-line chat-line-reply-chat" v-else> <div class="chat-line chat-line-reply-chat" v-else>
<div class="chat-line-inner"> <div class="chat-line-inner">
<div class="chat-icon"> <div class="chat-icon">
<img :src="data.icon" alt="ChatGPT"> <img :src="data.icon" alt="ChatGPT" />
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="content-wrapper"> <div class="content-wrapper">
<div class="content" v-html="md.render(processContent(data.content))"></div> <div
class="content"
v-html="md.render(processContent(data.content))"
></div>
</div> </div>
<div class="bar" v-if="data.created_at"> <div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>--> ><el-icon><Clock /></el-icon>
{{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg"> <span class="bar-item bg">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="复制回答" content="复制回答"
placement="bottom" placement="bottom"
> >
<el-icon class="copy-reply" :data-clipboard-text="data.content"> <el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy/> <DocumentCopy />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</span> </span>
<span v-if="!readOnly"> <span v-if="!readOnly">
<span class="bar-item bg" @click="reGenerate(data.prompt)"> <span class="bar-item bg" @click="reGenerate(data.prompt)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="重新生成" content="重新生成"
placement="bottom" placement="bottom"
> >
<el-icon><Refresh/></el-icon> <el-icon><Refresh /></el-icon>
</el-tooltip> </el-tooltip>
</span> </span>
<span class="bar-item bg" @click="synthesis(data.content)"> <span class="bar-item bg" @click="synthesis(data.content)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="生成语音朗读" content="生成语音朗读"
placement="bottom" placement="bottom"
> >
<i class="iconfont icon-speaker"></i> <i class="iconfont icon-speaker"></i>
</el-tooltip> </el-tooltip>
</span> </span>
</span> </span>
</div> </div>
</div> </div>
@@ -116,9 +128,9 @@
</template> </template>
<script setup> <script setup>
import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue"; import { Clock, DocumentCopy, Refresh } from "@element-plus/icons-vue";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {dateFormat, processContent} from "@/utils/libs"; import { dateFormat, processContent } from "@/utils/libs";
import hl from "highlight.js"; import hl from "highlight.js";
// eslint-disable-next-line no-undef,no-unused-vars // eslint-disable-next-line no-undef,no-unused-vars
const props = defineProps({ const props = defineProps({
@@ -128,8 +140,8 @@ const props = defineProps({
icon: "", icon: "",
content: "", content: "",
created_at: "", created_at: "",
tokens: 0, tokens: 0
}, }
}, },
readOnly: { readOnly: {
type: Boolean, type: Boolean,
@@ -137,53 +149,57 @@ const props = defineProps({
}, },
listStyle: { listStyle: {
type: String, type: String,
default: 'list', default: "list"
}, }
}) });
const mathjaxPlugin = require('markdown-it-mathjax3') const mathjaxPlugin = require("markdown-it-mathjax3");
const md = require('markdown-it')({ const md = require("markdown-it")({
breaks: true, breaks: true,
html: true, html: true,
linkify: true, linkify: true,
typographer: true, typographer: true,
highlight: function (str, lang) { highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000) const codeIndex =
parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
// 显示复制代码按钮 // 显示复制代码按钮
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span> const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '&lt;/textarea>')}</textarea>` <textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
if (lang && hl.getLanguage(lang)) { if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>` const langHtml = `<span class="lang-name">${lang}</span>`;
// 处理代码高亮 // 处理代码高亮
const preCode = hl.highlight(lang, str, true).value const preCode = hl.highlight(lang, str, true).value;
// 将代码包裹在 pre 中 // 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>` return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
} }
// 处理代码高亮 // 处理代码高亮
const preCode = md.utils.escapeHtml(str) const preCode = md.utils.escapeHtml(str);
// 将代码包裹在 pre 中 // 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>` return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
} }
}); });
md.use(mathjaxPlugin) md.use(mathjaxPlugin);
const emits = defineEmits(['regen']); const emits = defineEmits(["regen"]);
if (!props.data.icon) { if (!props.data.icon) {
props.data.icon = "images/gpt-icon.png" props.data.icon = "images/gpt-icon.png";
} }
const synthesis = (text) => { const synthesis = (text) => {
console.log(text) console.log(text);
ElMessage.info("语音合成功能暂不可用") ElMessage.info("语音合成功能暂不可用");
} };
// 重新生成 // 重新生成
const reGenerate = (prompt) => { const reGenerate = (prompt) => {
console.log(prompt) console.log(prompt);
emits('regen', prompt) emits("regen", prompt);
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">
@@ -191,11 +207,12 @@ const reGenerate = (prompt) => {
.chat-page,.chat-export { .chat-page,.chat-export {
.chat-line-reply-list { .chat-line-reply-list {
justify-content: center; justify-content: center;
background-color: rgba(247, 247, 248, 1); background-color: var(--chat-list-bg);
color:var(--theme-text-color-primary);
width 100% width 100%
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
padding-top: 1.5rem; padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3; border-bottom: 0.5px solid var(--el-border-color);
.chat-line-inner { .chat-line-inner {
display flex; display flex;
@@ -209,7 +226,7 @@ const reGenerate = (prompt) => {
img { img {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 10px; border-radius: 50%;
padding: 1px; padding: 1px;
} }
} }
@@ -224,7 +241,7 @@ const reGenerate = (prompt) => {
min-height 20px; min-height 20px;
word-break break-word; word-break break-word;
padding: 0 padding: 0
color #374151; color:var(--theme-text-color-primary);
font-size: var(--content-font-size); font-size: var(--content-font-size);
border-radius: 5px; border-radius: 5px;
overflow auto; overflow auto;
@@ -238,7 +255,7 @@ const reGenerate = (prompt) => {
line-height 1.5 line-height 1.5
code { code {
color #374151 color:var(--theme-text-color-primary);
background-color #e7e7e8 background-color #e7e7e8
padding 0 3px; padding 0 3px;
border-radius 5px; border-radius 5px;
@@ -296,7 +313,8 @@ const reGenerate = (prompt) => {
color #212529 color #212529
border-collapse collapse; border-collapse collapse;
border 1px solid #dee2e6; border 1px solid #dee2e6;
background-color #ffffff background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
thead { thead {
th { th {
@@ -396,10 +414,13 @@ const reGenerate = (prompt) => {
min-height 20px; min-height 20px;
word-break break-word; word-break break-word;
padding: 1rem padding: 1rem
color #374151; color var(--theme-text-primary);
font-size: var(--content-font-size); font-size: var(--content-font-size);
overflow auto; overflow auto;
background-color #F5F5F5 // background-color #F5F5F5
background-color :var(--chat-content-bg);
border-radius: 0 10px 10px 10px; border-radius: 0 10px 10px 10px;
img { img {
@@ -411,7 +432,7 @@ const reGenerate = (prompt) => {
line-height 1.5 line-height 1.5
code { code {
color #374151 color:var(--theme-text-color-primary);
background-color #e7e7e8 background-color #e7e7e8
padding 0 3px; padding 0 3px;
border-radius 5px; border-radius 5px;
@@ -469,7 +490,8 @@ const reGenerate = (prompt) => {
color #212529 color #212529
border-collapse collapse; border-collapse collapse;
border 1px solid #dee2e6; border 1px solid #dee2e6;
background-color #ffffff background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
thead { thead {
th { th {
@@ -541,5 +563,4 @@ const reGenerate = (prompt) => {
} }
} }
</style> </style>

View File

@@ -2,22 +2,27 @@
<el-container class="chat-file-list"> <el-container class="chat-file-list">
<div v-for="file in fileList" :key="file.url"> <div v-for="file in fileList" :key="file.url">
<div class="image" v-if="isImage(file.ext)"> <div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover"/> <el-image :src="file.url" fit="cover" />
<div class="action"> <div class="action">
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon> <el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
</div> </div>
</div> </div>
<div class="item" v-else> <div class="item" v-else>
<div class="icon"> <div class="icon">
<el-image :src="GetFileIcon(file.ext)" fit="cover" /> <el-image :src="GetFileIcon(file.ext)" fit="cover" />
</div> </div>
<div class="body"> <div class="body">
<div class="title"> <div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{substr(file.name, 30)}}</el-link> <el-link
:href="file.url"
target="_blank"
style="--el-font-weight-primary: bold"
>{{ substr(file.name, 30) }}</el-link
>
</div> </div>
<div class="info"> <div class="info">
<span>{{GetFileType(file.ext)}}</span> <span>{{ GetFileType(file.ext) }}</span>
<span>{{FormatFileSize(file.size)}}</span> <span>{{ FormatFileSize(file.size) }}</span>
</div> </div>
</div> </div>
<div class="action"> <div class="action">
@@ -29,26 +34,28 @@
</template> </template>
<script setup> <script setup>
import {ref} from "vue"; import { ref } from "vue";
import {CircleCloseFilled} from "@element-plus/icons-vue"; import { CircleCloseFilled } from "@element-plus/icons-vue";
import {isImage, removeArrayItem, substr} from "@/utils/libs"; import { isImage, removeArrayItem, substr } from "@/utils/libs";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system"; import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
const props = defineProps({ const props = defineProps({
files: { files: {
type: Array, type: Array,
default:[], default: []
} }
}) });
const emits = defineEmits(['removeFile']); const emits = defineEmits(["removeFile"]);
const fileList = ref(props.files) const fileList = ref(props.files);
const removeFile = (file) => { const removeFile = (file) => {
fileList.value = removeArrayItem(fileList.value, file, (v1,v2) => v1.url===v2.url) fileList.value = removeArrayItem(
emits('removeFile', file) fileList.value,
} file,
(v1, v2) => v1.url === v2.url
);
emits("removeFile", file);
};
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
@@ -75,7 +82,8 @@ const removeFile = (file) => {
display flex display flex
flex-flow row flex-flow row
border-radius 10px border-radius 10px
background-color #ffffff background-color:var(--chat-content-bg);
color:var(--theme-text-color-primary);
border 1px solid #e3e3e3 border 1px solid #e3e3e3
padding 6px padding 6px
margin-right 10px margin-right 10px
@@ -112,5 +120,4 @@ const removeFile = (file) => {
font-size 20px font-size 20px
} }
} }
</style>
</style>

View File

@@ -4,27 +4,32 @@
<i class="iconfont icon-attachment-st"></i> <i class="iconfont icon-attachment-st"></i>
</a> </a>
<el-dialog <el-dialog
class="file-list-dialog" class="file-list-dialog"
v-model="show" v-model="show"
:close-on-click-modal="true" :close-on-click-modal="true"
:show-close="true" :show-close="true"
:width="800" :width="800"
title="文件管理" title="文件管理"
> >
<el-scrollbar ref="scrollbarRef" max-height="80vh" style="height: 100%;" @scroll="onScroll"> <el-scrollbar
ref="scrollbarRef"
max-height="80vh"
style="height: 100%"
@scroll="onScroll"
>
<div class="file-list"> <div class="file-list">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="3"> <el-col :span="3">
<div class="grid-content"> <div class="grid-content">
<el-upload <el-upload
class="avatar-uploader" class="avatar-uploader"
:auto-upload="true" :auto-upload="true"
:show-file-list="false" :show-file-list="false"
:http-request="afterRead" :http-request="afterRead"
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3" accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
> >
<el-icon class="avatar-uploader-icon"> <el-icon class="avatar-uploader-icon">
<Plus/> <Plus />
</el-icon> </el-icon>
</el-upload> </el-upload>
</div> </div>
@@ -32,116 +37,139 @@
<el-col :span="3" v-for="file in fileData.items" :key="file.url"> <el-col :span="3" v-for="file in fileData.items" :key="file.url">
<div class="grid-content"> <div class="grid-content">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
:content="file.name" :content="file.name"
placement="top"> placement="top"
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/> >
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/> <el-image
:src="file.url"
fit="cover"
v-if="isImage(file.ext)"
@click="insertURL(file)"
/>
<el-image
:src="GetFileIcon(file.ext)"
fit="cover"
v-else
@click="insertURL(file)"
/>
</el-tooltip> </el-tooltip>
<div class="opt"> <div class="opt">
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/> <el-button
type="danger"
size="small"
:icon="Delete"
@click="removeFile(file)"
circle
/>
</div> </div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row justify="center" v-if="!fileData.isLastPage" @click="fetchFiles(fileData.page)"> <el-row
justify="center"
v-if="!fileData.isLastPage"
@click="fetchFiles(fileData.page)"
>
<el-link>加载更多</el-link> <el-link>加载更多</el-link>
</el-row> </el-row>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-dialog> </el-dialog>
</el-container> </el-container>
</template> </template>
<script setup> <script setup>
import {reactive, ref} from "vue"; import { reactive, ref } from "vue";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {httpGet, httpPost} from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
import {Delete, Plus} from "@element-plus/icons-vue"; import { Delete, Plus } from "@element-plus/icons-vue";
import {isImage, removeArrayItem} from "@/utils/libs"; import { isImage, removeArrayItem } from "@/utils/libs";
import {GetFileIcon} from "@/store/system"; import { GetFileIcon } from "@/store/system";
const props = defineProps({ const props = defineProps({
userId: Number, userId: Number
}); });
const emits = defineEmits(['selected']); const emits = defineEmits(["selected"]);
const show = ref(false) const show = ref(false);
const scrollbarRef = ref(null) const scrollbarRef = ref(null);
const fileData = reactive({ const fileData = reactive({
items:[], items: [],
page: 1, page: 1,
isLastPage: true, isLastPage: true
}) });
const fetchFiles = (pageNo) => { const fetchFiles = (pageNo) => {
if(pageNo === 1) show.value = true if (pageNo === 1) show.value = true;
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 }).then(res => { httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 })
const { items, page, total_page } = res.data .then((res) => {
const { items, page, total_page } = res.data;
if(page === 1){ if (page === 1) {
fileData.items = items fileData.items = items;
}else{ } else {
fileData.items = [...fileData.items, ...items] fileData.items = [...fileData.items, ...items];
} }
fileData.isLastPage = (page === total_page) fileData.isLastPage = page === total_page;
if(!fileData.isLastPage){ if (!fileData.isLastPage) {
fileData.page = page + 1 fileData.page = page + 1;
} }
})
}).catch(() => { .catch(() => {});
}) };
}
// el-scrollbar 滚动回调 // el-scrollbar 滚动回调
const onScroll = (options) => { const onScroll = (options) => {
const wrapRef = scrollbarRef.value.wrapRef const wrapRef = scrollbarRef.value.wrapRef;
scrollbarRef.value.moveY = wrapRef.scrollTop * 100 / wrapRef.clientHeight scrollbarRef.value.moveY = (wrapRef.scrollTop * 100) / wrapRef.clientHeight;
scrollbarRef.value.moveX = wrapRef.scrollLeft * 100 / wrapRef.clientWidth scrollbarRef.value.moveX = (wrapRef.scrollLeft * 100) / wrapRef.clientWidth;
const poor = wrapRef.scrollHeight - wrapRef.clientHeight const poor = wrapRef.scrollHeight - wrapRef.clientHeight;
// 判断滚动到底部 自动加载数据 // 判断滚动到底部 自动加载数据
if (options.scrollTop + 2 >= poor && !fileData.isLastPage) { if (options.scrollTop + 2 >= poor && !fileData.isLastPage) {
fetchFiles(fileData.page) fetchFiles(fileData.page);
} }
} };
const afterRead = (file) => { const afterRead = (file) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file.file, file.name); formData.append("file", file.file, file.name);
// 执行上传操作 // 执行上传操作
httpPost('/api/upload', formData).then((res) => { httpPost("/api/upload", formData)
fileData.items.unshift(res.data) .then((res) => {
ElMessage.success({message: "上传成功", duration: 500}) fileData.items.unshift(res.data);
}).catch((e) => { ElMessage.success({ message: "上传成功", duration: 500 });
ElMessage.error('图片上传失败:' + e.message) })
}) .catch((e) => {
ElMessage.error("图片上传失败:" + e.message);
});
}; };
const removeFile = (file) => { const removeFile = (file) => {
httpGet('/api/upload/remove?id=' + file.id).then(() => { httpGet("/api/upload/remove?id=" + file.id)
fileData.items = removeArrayItem(fileData.items, file, (v1, v2) => { .then(() => {
return v1.id === v2.id fileData.items = removeArrayItem(fileData.items, file, (v1, v2) => {
return v1.id === v2.id;
});
ElMessage.success("文件删除成功!");
fetchFiles(1);
}) })
ElMessage.success("文件删除成功!") .catch((e) => {
fetchFiles(1) ElMessage.error("文件删除失败:" + e.message);
}).catch((e) => { });
ElMessage.error('文件删除失败:' + e.message) };
})
}
const insertURL = (file) => { const insertURL = (file) => {
show.value = false show.value = false;
// 如果是相对路径,处理成绝对路径 // 如果是相对路径,处理成绝对路径
if (file.url.indexOf("http") === -1) { if (file.url.indexOf("http") === -1) {
file.url = location.protocol + "//" + location.host + file.url file.url = location.protocol + "//" + location.host + file.url;
} }
emits('selected', file) emits("selected", file);
} };
</script> </script>
<style lang="stylus"> <style lang="stylus">
@@ -149,7 +177,7 @@ const insertURL = (file) => {
.file-select-box { .file-select-box {
.file-upload-img { .file-upload-img {
.iconfont { .iconfont {
font-size: 24px; font-size: 19px;
} }
} }
@@ -215,4 +243,4 @@ const insertURL = (file) => {
} }
} }
} }
</style> </style>

View File

@@ -13,7 +13,9 @@
<div class="list-box"> <div class="list-box">
<ul> <ul>
<li v-for="item in samples" :key="item"><a @click="send(item)">{{ item }}</a></li> <li v-for="item in samples" :key="item">
<a @click="send(item)">{{ item }}</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -27,7 +29,9 @@
<div class="list-box"> <div class="list-box">
<ul> <ul>
<li v-for="item in plugins" :key="item.value"><a @click="send(item.value)">{{ item.text }}</a></li> <li v-for="item in plugins" :key="item.value">
<a @click="send(item.value)">{{ item.text }}</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -54,19 +58,18 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { getSystemInfo } from "@/store/cache";
import {onMounted, ref} from "vue"; const title = ref(process.env.VUE_APP_TITLE);
import {ElMessage} from "element-plus"; const version = ref(process.env.VUE_APP_VERSION);
import {getSystemInfo} from "@/store/cache";
const title = ref(process.env.VUE_APP_TITLE)
const version = ref(process.env.VUE_APP_VERSION)
const samples = ref([ const samples = ref([
"用小学生都能听懂的术语解释什么是量子纠缠", "用小学生都能听懂的术语解释什么是量子纠缠",
"能给一位6岁男孩的生日会提供一些创造性的建议吗", "能给一位6岁男孩的生日会提供一些创造性的建议吗",
"如何用 Go 语言实现支持代理 Http client 请求?" "如何用 Go 语言实现支持代理 Http client 请求?"
]) ]);
const plugins = ref([ const plugins = ref([
{ {
@@ -81,7 +84,7 @@ const plugins = ref([
value: "今日头条", value: "今日头条",
text: "今日头条:给用户推荐当天的头条新闻,周榜热文" text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
} }
]) ]);
const capabilities = ref([ const capabilities = ref([
{ {
@@ -96,20 +99,22 @@ const capabilities = ref([
text: "绘画马斯克开拖拉机20世纪中国农村。3:2", text: "绘画马斯克开拖拉机20世纪中国农村。3:2",
value: "绘画马斯克开拖拉机20世纪中国农村。3:2" value: "绘画马斯克开拖拉机20世纪中国农村。3:2"
} }
]) ]);
onMounted(() => { onMounted(() => {
getSystemInfo().then(res => { getSystemInfo()
title.value = res.data.title .then((res) => {
}).catch(e => { title.value = res.data.title;
ElMessage.error("获取系统配置失败:" + e.message) })
}) .catch((e) => {
}) ElMessage.error("获取系统配置失败:" + e.message);
});
});
const emits = defineEmits(['send']); const emits = defineEmits(["send"]);
const send = (text) => { const send = (text) => {
emits('send', text) emits("send", text);
} };
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.welcome { .welcome {
@@ -148,10 +153,9 @@ const send = (text) => {
font-size 14px; font-size 14px;
padding .75rem padding .75rem
border-radius 5px; border-radius 5px;
background-color: rgba(247, 247, 248, 1); background-color: var(--chat-wel-bg);
color:var( --theme-text-color-secondary);
line-height 1.5 line-height 1.5
color #666666
a { a {
cursor pointer cursor pointer
@@ -165,4 +169,4 @@ const send = (text) => {
} }
} }
} }
</style> </style>

View File

@@ -1,39 +1,41 @@
<template> <template>
<div :class="'admin-header '+theme"> <div :class="'admin-header ' + theme">
<!-- 折叠按钮 --> <!-- 折叠按钮 -->
<div class="collapse-btn" @click="collapseChange"> <div class="collapse-btn" @click="collapseChange">
<el-icon v-if="sidebar.collapse"> <el-icon v-if="sidebar.collapse">
<Expand/> <Expand />
</el-icon> </el-icon>
<el-icon v-else> <el-icon v-else>
<Fold/> <Fold />
</el-icon> </el-icon>
</div> </div>
<div class="breadcrumb"> <div class="breadcrumb">
<el-breadcrumb :separator-icon="ArrowRight"> <el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item v-for="item in breadcrumb">{{ item.title }}</el-breadcrumb-item> <el-breadcrumb-item v-for="item in breadcrumb">{{
item.title
}}</el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-user-con"> <div class="header-user-con">
<!-- 切换主题 --> <!-- 切换主题 -->
<el-switch <el-switch
style="margin-right: 10px" style="margin-right: 10px"
v-model="dark" v-model="dark"
inline-prompt inline-prompt
:active-action-icon="Moon" :active-action-icon="Moon"
:inactive-action-icon="Sunny" :inactive-action-icon="Sunny"
@change="changeTheme" @change="changeTheme"
/> />
<!-- 用户名下拉菜单 --> <!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" :hide-on-click="true" trigger="click"> <el-dropdown class="user-name" :hide-on-click="true" trigger="click">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<el-avatar class="user-avatar" :size="30" :src="avatar"/> <el-avatar class="user-avatar" :size="30" :src="avatar" />
<el-icon class="el-icon--right"> <el-icon class="el-icon--right">
<arrow-down/> <arrow-down />
</el-icon> </el-icon>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item> <el-dropdown-item>
@@ -48,82 +50,91 @@
</el-dropdown> </el-dropdown>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {onMounted, ref, watch} from 'vue'; import { onMounted, ref, watch } from "vue";
import {getMenuItems, useSidebarStore} from '@/store/sidebar'; import { getMenuItems, useSidebarStore } from "@/store/sidebar";
import {useRouter} from "vue-router"; import { useRouter } from "vue-router";
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue"; import {
import {httpGet} from "@/utils/http"; ArrowDown,
import {ElMessage} from "element-plus"; ArrowRight,
import {removeAdminToken} from "@/store/session"; Expand,
import {useSharedStore} from "@/store/sharedata"; Fold,
Moon,
Sunny
} from "@element-plus/icons-vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { removeAdminToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
const version = ref(process.env.VUE_APP_VERSION) const version = ref(process.env.VUE_APP_VERSION);
const avatar = ref('/images/user-info.jpg') const avatar = ref("/images/user-info.jpg");
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
const router = useRouter(); const router = useRouter();
const breadcrumb = ref([]) const breadcrumb = ref([]);
const store = useSharedStore() const store = useSharedStore();
const dark = ref(store.adminTheme === 'dark') const dark = ref(store.adminTheme === "dark");
const theme = ref(store.adminTheme) const theme = ref(store.adminTheme);
watch(() => store.adminTheme, (val) => { watch(
theme.value = val () => store.adminTheme,
}) (val) => {
theme.value = val;
}
);
const changeTheme = () => { const changeTheme = () => {
store.setAdminTheme(dark.value ? 'dark' : 'light') store.setAdminTheme(dark.value ? "dark" : "light");
} };
router.afterEach((to) => { router.afterEach((to) => {
initBreadCrumb(to.path) initBreadCrumb(to.path);
}); });
onMounted(() => { onMounted(() => {
initBreadCrumb(router.currentRoute.value.path) initBreadCrumb(router.currentRoute.value.path);
}) });
// 初始化面包屑导航 // 初始化面包屑导航
const initBreadCrumb = (path) => { const initBreadCrumb = (path) => {
breadcrumb.value = [{title: "首页"}] breadcrumb.value = [{ title: "首页" }];
const items = getMenuItems() const items = getMenuItems();
if (items) { if (items) {
let bk = false let bk = false;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].index === path) { if (items[i].index === path) {
breadcrumb.value.push({ breadcrumb.value.push({
title: items[i].title, title: items[i].title,
path: items[i].index path: items[i].index
}) });
break break;
} }
if (bk) { if (bk) {
break break;
} }
if (items[i]['subs']) { if (items[i]["subs"]) {
const subs = items[i]['subs'] const subs = items[i]["subs"];
for (let j = 0; j < subs.length; j++) { for (let j = 0; j < subs.length; j++) {
if (subs[j].index === path) { if (subs[j].index === path) {
breadcrumb.value.push({ breadcrumb.value.push({
title: items[i].title, title: items[i].title,
path: items[i].index path: items[i].index
}) });
breadcrumb.value.push({ breadcrumb.value.push({
title: subs[j].title, title: subs[j].title,
path: subs[j].index path: subs[j].index
}) });
bk = true bk = true;
break break;
} }
} }
} }
} }
} }
} };
// 侧边栏折叠 // 侧边栏折叠
const collapseChange = () => { const collapseChange = () => {
@@ -137,13 +148,15 @@ onMounted(() => {
}); });
const logout = function () { const logout = function () {
httpGet("/api/admin/logout").then(() => { httpGet("/api/admin/logout")
removeAdminToken() .then(() => {
router.replace('/admin/login') removeAdminToken();
}).catch((e) => { router.replace("/admin/login");
ElMessage.error("注销失败: " + e.message); })
}) .catch((e) => {
} ElMessage.error("注销失败: " + e.message);
});
};
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.admin-header { .admin-header {
@@ -152,8 +165,8 @@ const logout = function () {
overflow hidden overflow hidden
height: 50px; height: 50px;
font-size: 22px; font-size: 22px;
color: #303133; background-color:var(--chat-content-bg);
background-color #ffffff color:var(--theme-text-color-primary);
.collapse-btn { .collapse-btn {
display: flex; display: flex;
@@ -260,5 +273,4 @@ const logout = function () {
.admin-header { .admin-header {
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="mobile-message-mj"> <div class="mobile-message-mj">
<div class="chat-icon"> <div class="chat-icon">
<van-image :src="icon"/> <van-image :src="icon" />
</div> </div>
<div class="chat-item"> <div class="chat-item">
@@ -11,21 +11,30 @@
<div class="content-inner"> <div class="content-inner">
<div class="text" v-html="data.html"></div> <div class="text" v-html="data.html"></div>
<div class="images" v-if="data.image?.url !== ''"> <div class="images" v-if="data.image?.url !== ''">
<el-image :src="data.image?.url" <el-image
:zoom-rate="1.2" :src="data.image?.url"
:preview-src-list="[data.image?.url]" :zoom-rate="1.2"
fit="cover" :preview-src-list="[data.image?.url]"
:initial-index="0" loading="lazy"> fit="cover"
:initial-index="0"
loading="lazy"
>
<template #placeholder> <template #placeholder>
<div class="image-slot" <div
:style="{height: height+'px', lineHeight:height+'px'}"> class="image-slot"
正在加载图片<span class="dot">...</span></div> :style="{
height: height + 'px',
lineHeight: height + 'px'
}"
>
正在加载图片<span class="dot">...</span>
</div>
</template> </template>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<el-icon> <el-icon>
<Picture/> <Picture />
</el-icon> </el-icon>
</div> </div>
</template> </template>
@@ -33,7 +42,7 @@
</div> </div>
</div> </div>
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''"> <div class="opt" v-if="data.showOpt && data.image?.hash !== ''">
<div class="opt-line"> <div class="opt-line">
<ul> <ul>
<li><a @click="upscale(1)">U1</a></li> <li><a @click="upscale(1)">U1</a></li>
@@ -54,17 +63,16 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref, watch} from "vue"; import { ref, watch } from "vue";
import {Picture} from "@element-plus/icons-vue"; import { Picture } from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http"; import { httpPost } from "@/utils/http";
import {getSessionId} from "@/store/session"; import { getSessionId } from "@/store/session";
import {showNotify} from "vant"; import { showNotify } from "vant";
const props = defineProps({ const props = defineProps({
content: Object, content: Object,
@@ -74,36 +82,40 @@ const props = defineProps({
createdAt: String createdAt: String
}); });
const data = ref(props.content) const data = ref(props.content);
const cacheKey = "img_placeholder_height" const cacheKey = "img_placeholder_height";
const item = localStorage.getItem(cacheKey); const item = localStorage.getItem(cacheKey);
const loading = ref(false) const loading = ref(false);
const height = ref(0) const height = ref(0);
if (item) { if (item) {
height.value = parseInt(item) height.value = parseInt(item);
} }
if (data.value["image"]?.width > 0) { if (data.value["image"]?.width > 0) {
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width height.value =
localStorage.setItem(cacheKey, height.value) (350 * data.value["image"]?.height) / data.value["image"]?.width;
localStorage.setItem(cacheKey, height.value);
} }
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1; data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
// console.log(data.value) // console.log(data.value)
watch(() => props.content, (newVal) => { watch(
data.value = newVal; () => props.content,
}); (newVal) => {
const emits = defineEmits(['disable-input', 'disable-input']); data.value = newVal;
}
);
const emits = defineEmits(["disable-input", "disable-input"]);
const upscale = (index) => { const upscale = (index) => {
send('/api/mj/upscale', index) send("/api/mj/upscale", index);
} };
const variation = (index) => { const variation = (index) => {
send('/api/mj/variation', index) send("/api/mj/variation", index);
} };
const send = (url, index) => { const send = (url, index) => {
loading.value = true loading.value = true;
emits('disable-input') emits("disable-input");
httpPost(url, { httpPost(url, {
index: index, index: index,
src: "chat", src: "chat",
@@ -114,15 +126,20 @@ const send = (url, index) => {
prompt: data.value?.["prompt"], prompt: data.value?.["prompt"],
chat_id: props.chatId, chat_id: props.chatId,
role_id: props.roleId, role_id: props.roleId,
icon: props.icon, icon: props.icon
}).then(() => {
showNotify({type: "success", message: "任务推送成功,请耐心等待任务执行..."})
loading.value = false
}).catch(e => {
showNotify({type: "danger", message: "任务推送失败:" + e.message})
emits('disable-input')
}) })
} .then(() => {
showNotify({
type: "success",
message: "任务推送成功,请耐心等待任务执行..."
});
loading.value = false;
})
.catch((e) => {
showNotify({ type: "danger", message: "任务推送失败:" + e.message });
emits("disable-input");
});
};
</script> </script>
<style lang="stylus"> <style lang="stylus">
@@ -268,4 +285,4 @@ const send = (url, index) => {
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,122 +1,195 @@
<template> <template>
<div class="home"> <div class="layout">
<div class="header"> <div class="tab-box">
<div class="banner"> <div class="flex-center-col big-top-title xxx">
<div class="logo"> <div class="flex-center-col" @click="isCollapse = !isCollapse">
<el-image :src="logo" @click="router.push('/')"/> <el-icon v-if="isCollapse" class="openicon">
<svg
t="1733138242826"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1853"
id="mx_n_1733138242827"
width="200"
height="200"
>
<path
d="M715 267c135.31 0 245 109.69 245 245S850.31 757 715 757H309C173.69 757 64 647.31 64 512s109.69-245 245-245h406zM309 367c-80.081 0-145 64.919-145 145s64.919 145 145 145 145-64.919 145-145-64.919-145-145-145z"
fill="#754ff6"
p-id="1854"
></path>
</svg>
</el-icon>
</div> </div>
<div class="title"> <div class="flex" :class="{ 'top-collapse': !isCollapse }">
<span>{{ title }}</span> <div class="top-avatar flex">
<span class="title" v-if="!isCollapse">GeekAI</span>
<img
v-if="loginUser.id"
:src="!!loginUser.avatar ? loginUser.avatar : avatarImg"
alt=""
:class="{ marr: !isCollapse }"
/>
</div>
<div class="menuIcon xxx" @click="isCollapse = !isCollapse">
<el-icon v-if="!isCollapse" class="openicon">
<svg
t="1733138405307"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2064"
width="200"
height="200"
>
<path
d="M715 267c135.31 0 245 109.69 245 245S850.31 757 715 757H309C173.69 757 64 647.31 64 512s109.69-245 245-245h406z m0 100c-80.081 0-145 64.919-145 145s64.919 145 145 145 145-64.919 145-145-64.919-145-145-145z"
fill="#754ff6"
p-id="2065"
></path>
</svg>
</el-icon>
</div>
</div> </div>
</div> </div>
<div class="navbar"> <div
<el-tooltip class="menu-list"
v-if="!license.de_copy" :style="{ width: isCollapse ? '65px' : '170px' }"
class="box-item" :class="{ 'menu-list-collapse': !isCollapse }"
effect="light" >
content="部署文档" <ul>
placement="bottom"> <li
<a :href="docsURL" class="link-button" target="_blank"> class="menu-list-item flex-center-col"
<i class="iconfont icon-book"></i> v-for="item in mainNavs"
</a> :key="item.url"
</el-tooltip> @click="changeNav(item)"
:class="item.url === curPath ? 'active' : ''"
<el-tooltip >
v-if="!license.de_copy" <el-image :src="item.icon" class="el-icon" />
class="box-item" <div
effect="light" class="menu-title"
content="项目源码" :class="{ 'menu-title-collapse': !isCollapse }"
placement="bottom"> >
<a href="https://github.com/yangjian102621/chatgpt-plus" class="link-button" target="_blank"> {{ item.name }}
<i class="iconfont icon-github"></i> </div>
</a>
</el-tooltip>
<el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="loginUser.id">
<span class="el-dropdown-link">
<el-image :src="loginUser.avatar"/>
</span>
<template #dropdown>
<el-dropdown-menu class="user-info-menu">
<el-dropdown-item @click="showConfigDialog = true">
<el-icon>
<UserFilled/>
</el-icon>
<span class="username">{{ loginUser.nickname }}</span>
</el-dropdown-item>
<div v-if="!license.de_copy">
<el-dropdown-item>
<i class="iconfont icon-book"></i>
<a :href="docsURL" target="_blank">
用户手册
</a>
</el-dropdown-item>
<el-dropdown-item>
<i class="iconfont icon-github"></i>
<a :href="gitURL" target="_blank">
GeekAI {{ version }}
</a>
</el-dropdown-item>
</div>
<el-divider style="margin: 2px 0"/>
<el-dropdown-item @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else>
<el-button size="small" color="#21aa93" @click="store.setShowLoginDialog(true)" round>登录</el-button>
</div>
</div>
</div>
<div class="main">
<div class="navigator">
<ul class="nav-items">
<li v-for="item in mainNavs" :key="item.url">
<el-tooltip
effect="light"
:content="item.name"
placement="right">
<a @click="changeNav(item)" :class="item.url === curPath ? 'active' : ''">
<el-image :src="item.icon" style="width: 30px;height: 30px"/>
</a>
</el-tooltip>
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</li> </li>
<el-popover <!-- <li
class="menu-list-item flex-center-col"
v-for="item in 5"
:key="item"
>
<el-icon><Location /></el-icon>
<div>首页</div>
</li> -->
<!-- 更多 -->
<div class="bot" :style="{ width: isCollapse ? '65px' : '170px' }">
<div class="bot-line"></div>
<el-popover
v-if="moreNavs.length > 0" v-if="moreNavs.length > 0"
placement="right-end" placement="right-end"
trigger="hover" trigger="hover"
> >
<template #reference> <template #reference>
<li> <li class="menu-list-item flex-center-col">
<a class="active"> <el-icon><CirclePlus /></el-icon>
<el-image src="/images/menu/more.png" style="width: 30px;height: 30px"/> <div class="menu-title">更多</div>
</a>
</li>
</template>
<template #default>
<ul class="more-menus">
<li v-for="item in moreNavs" :key="item.url" :class="item.url === curPath ? 'active' : ''">
<a @click="changeNav(item)">
<el-image :src="item.icon" style="width: 20px;height: 20px"/>
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</a>
</li> </li>
</ul> </template>
</template> <template #default>
</el-popover> <ul class="more-menus">
<li
v-for="(item, index) in moreNavs"
:key="item.url"
:class="{
active: item.url === curPath,
moreTitle: index !== 3 && index !== 4,
twoTittle: index === 3 || index === 4
}"
>
<a @click="changeNav(item)">
<el-image
:src="item.icon"
style="width: 20px; height: 20px"
/>
<span
:class="item.url === curPath ? 'title active' : 'title'"
>{{ item.name }}</span
>
</a>
</li>
</ul>
</template>
</el-popover>
<el-popover
placement="right-end"
trigger="hover"
v-if="loginUser.id"
>
<template #reference>
<li class="menu-list-item flex-center-col">
<el-icon><Setting /></el-icon>
<div v-if="!isCollapse">设置</div>
</li>
</template>
<template #default>
<ul class="more-menus setting-menus">
<li>
<div @click="showConfigDialog = true" class="flex">
<el-icon>
<UserFilled />
</el-icon>
<span class="username title">{{
loginUser.nickname
}}</span>
</div>
</li>
<li>
<a @click="logout" class="flex">
<i class="iconfont icon-logout"></i>
<span class="title">退出登录</span>
</a>
</li>
</ul>
</template>
</el-popover>
<li class="menu-bot-item">
<a
:href="gitURL"
class="link-button"
target="_blank"
v-if="!license.de_copy && !isCollapse"
>
<i class="iconfont icon-github"></i>
</a>
<a @click="router.push('/')" class="link-button">
<i class="iconfont icon-house"></i>
</a>
<ThemeChange />
<!-- <div v-if="!isCollapse">会员</div> -->
</li>
</div>
</ul> </ul>
</div> </div>
</div>
<div class="content custom-scroll" :style="{height: mainWinHeight+'px'}"> <div class="right-main">
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button
@click="router.push('/login')"
class="btn-go animate__animated animate__pulse animate__infinite"
round
>登录</el-button
>
</div>
<div class="content custom-scroll">
<router-view :key="routerViewKey" v-slot="{ Component }"> <router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in"> <transition name="move" mode="out-in">
<component :is="Component"></component> <component :is="Component"></component>
@@ -124,120 +197,143 @@
</router-view> </router-view>
</div> </div>
</div> </div>
<config-dialog
<login-dialog :show="show" @hide="store.setShowLoginDialog(false)" @success="loginCallback"/> v-if="loginUser.id"
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false"/> :show="showConfigDialog"
@hide="showConfigDialog = false"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { CirclePlus, Setting } from "@element-plus/icons-vue";
import {useRouter} from "vue-router"; import ThemeChange from "@/components/ThemeChange.vue";
import {onMounted, ref, watch} from "vue"; import { avatarImg } from "@/assets/img/avatar.jpg";
import {httpGet} from "@/utils/http"; import { useRouter } from "vue-router";
import {ElMessage} from "element-plus"; import { onMounted, ref, watch } from "vue";
import {UserFilled} from "@element-plus/icons-vue"; import { httpGet } from "@/utils/http";
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache"; import { ElMessage } from "element-plus";
import {removeUserToken} from "@/store/session"; import { UserFilled } from "@element-plus/icons-vue";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue"; import LoginDialog from "@/components/LoginDialog.vue";
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue"; import ConfigDialog from "@/components/UserInfoDialog.vue";
import {showMessageError} from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
const isCollapse = ref(true);
const router = useRouter(); const router = useRouter();
const logo = ref(''); const logo = ref("");
const mainNavs = ref([]) const mainNavs = ref([]);
const moreNavs = ref([]) const moreNavs = ref([]);
const curPath = ref(router.currentRoute.value.path) const curPath = ref(router.currentRoute.value.path);
const title = ref("") const title = ref("");
const mainWinHeight = window.innerHeight - 50 // const mainWinHeight = window.innerHeight - 50;
const loginUser = ref({})
const version = ref(process.env.VUE_APP_VERSION) const loginUser = ref({});
const routerViewKey = ref(0) const mainWinHeight = loginUser.value.id
const showConfigDialog = ref(false) ? window.innerHeight
const license = ref({de_copy: true}) : window.innerHeight;
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL) const version = ref(process.env.VUE_APP_VERSION);
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const docsURL = ref(process.env.VUE_APP_DOCS_URL);
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const store = useSharedStore(); const store = useSharedStore();
const show = ref(false) const show = ref(false);
watch(() => store.showLoginDialog, (newValue) => { watch(
show.value = newValue () => store.showLoginDialog,
}); (newValue) => {
show.value = newValue;
}
);
// 监听路由变化 // 监听路由变化
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
curPath.value = to.path curPath.value = to.path;
next(); next();
}); });
if (curPath.value === "/external") { if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url curPath.value = router.currentRoute.value.query.url;
} }
const changeNav = (item) => { const changeNav = (item) => {
curPath.value = item.url curPath.value = item.url;
if (item.url.indexOf("http") !== -1) { // 外部链接 if (item.url.indexOf("http") !== -1) {
router.push({name: 'ExternalLink', query: {url: item.url}}) // 外部链接
router.push({ name: "ExternalLink", query: { url: item.url } });
} else { } else {
router.push(item.url) router.push(item.url);
} }
} };
onMounted(() => { onMounted(() => {
getSystemInfo().then(res => { getSystemInfo()
logo.value = res.data.logo .then((res) => {
title.value = res.data.title logo.value = res.data.logo;
}).catch(e => { title.value = res.data.title;
ElMessage.error("获取系统配置失败:" + e.message) })
}) .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
// 获取菜单 // 获取菜单
httpGet("/api/menu/list").then(res => { httpGet("/api/menu/list")
mainNavs.value = res.data .then((res) => {
// 根据窗口的高度计算应该显示多少菜单 mainNavs.value = res.data;
const rows = Math.floor((window.innerHeight - 100) / 90) // 根据窗口的高度计算应该显示多少菜单
if (res.data.length > rows) { const rows = Math.floor((window.innerHeight - 100) / 90);
mainNavs.value = res.data.slice(0, rows) if (res.data.length > rows) {
moreNavs.value = res.data.slice(rows) mainNavs.value = res.data.slice(0, rows);
} moreNavs.value = res.data.slice(rows);
}).catch(e => { }
ElMessage.error("获取系统菜单失败:" + e.message) })
}) .catch((e) => {
ElMessage.error("获取系统菜单失败:" + e.message);
});
getLicenseInfo().then(res => { getLicenseInfo()
license.value = res.data .then((res) => {
}).catch(e => { license.value = res.data;
license.value = {de_copy: false} })
showMessageError("获取 License 配置:" + e.message) .catch((e) => {
}) license.value = { de_copy: false };
showMessageError("获取 License 配置:" + e.message);
});
init() init();
}) });
const init = () => { const init = () => {
checkSession().then(user => { checkSession()
loginUser.value = user .then((user) => {
}).catch(() => { loginUser.value = user;
}) })
} .catch(() => {});
};
const logout = function () { const logout = function () {
httpGet('/api/user/logout').then(() => { httpGet("/api/user/logout")
removeUserToken() .then(() => {
store.setShowLoginDialog(true) removeUserToken();
store.setIsLogin(false) store.setShowLoginDialog(true);
loginUser.value = {} store.setIsLogin(false);
// 刷新组件 loginUser.value = {};
routerViewKey.value += 1 // 刷新组件
}).catch(() => { routerViewKey.value += 1;
ElMessage.error('注销失败!'); })
}) .catch(() => {
} ElMessage.error("注销失败!");
});
};
const loginCallback = () => { const loginCallback = () => {
init() init();
// 刷新组件 // 刷新组件
routerViewKey.value += 1 routerViewKey.value += 1;
} };
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@@ -0,0 +1,297 @@
<template>
<div class="home">
<div class="header">
<div class="banner">
<div class="logo">
<el-image :src="logo" @click="router.push('/')" />
</div>
<div class="title">
<span>{{ title }}</span>
</div>
</div>
<div class="navbar">
<el-tooltip
v-if="!license.de_copy"
class="box-item"
effect="light"
content="部署文档"
placement="bottom"
>
<a :href="docsURL" class="link-button" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip
v-if="!license.de_copy"
class="box-item"
effect="light"
content="项目源码"
placement="bottom"
>
<a
href="https://github.com/yangjian102621/chatgpt-plus"
class="link-button"
target="_blank"
>
<i class="iconfont icon-github"></i>
</a>
</el-tooltip>
<el-dropdown
:hide-on-click="true"
class="user-info"
trigger="click"
v-if="loginUser.id"
>
<span class="el-dropdown-link">
<el-image :src="loginUser.avatar" />
</span>
<template #dropdown>
<el-dropdown-menu class="user-info-menu">
<el-dropdown-item @click="showConfigDialog = true">
<el-icon>
<UserFilled />
</el-icon>
<span class="username">{{ loginUser.nickname }}</span>
</el-dropdown-item>
<div v-if="!license.de_copy">
<el-dropdown-item>
<i class="iconfont icon-book"></i>
<a :href="docsURL" target="_blank"> 用户手册 </a>
</el-dropdown-item>
<el-dropdown-item>
<i class="iconfont icon-github"></i>
<a :href="gitURL" target="_blank"> GeekAI {{ version }} </a>
</el-dropdown-item>
</div>
<el-divider style="margin: 2px 0" />
<el-dropdown-item @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else>
<el-button
size="small"
color="#21aa93"
@click="store.setShowLoginDialog(true)"
round
>登录</el-button
>
</div>
</div>
</div>
<div class="main">
<div class="navigator">
<ul class="nav-items">
<li v-for="item in mainNavs" :key="item.url">
<el-tooltip effect="light" :content="item.name" placement="right">
<a
@click="changeNav(item)"
:class="item.url === curPath ? 'active' : ''"
>
<el-image :src="item.icon" style="width: 30px; height: 30px" />
</a>
</el-tooltip>
<span :class="item.url === curPath ? 'title active' : 'title'">{{
item.name
}}</span>
</li>
<el-popover
v-if="moreNavs.length > 0"
placement="right-end"
trigger="hover"
>
<template #reference>
<li>
<a class="active">
<el-image
src="/images/menu/more.png"
style="width: 30px; height: 30px"
/>
</a>
</li>
</template>
<template #default>
<ul class="more-menus">
<li
v-for="item in moreNavs"
:key="item.url"
:class="item.url === curPath ? 'active' : ''"
>
<a @click="changeNav(item)">
<el-image
:src="item.icon"
style="width: 20px; height: 20px"
/>
<span
:class="item.url === curPath ? 'title active' : 'title'"
>{{ item.name }}</span
>
</a>
</li>
</ul>
</template>
</el-popover>
</ul>
</div>
<div
class="content custom-scroll"
:style="{ height: mainWinHeight + 'px' }"
>
<router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
</div>
</div>
<login-dialog
:show="show"
@hide="store.setShowLoginDialog(false)"
@success="loginCallback"
/>
<config-dialog
v-if="loginUser.id"
:show="showConfigDialog"
@hide="showConfigDialog = false"
/>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { UserFilled } from "@element-plus/icons-vue";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import { showMessageError } from "@/utils/dialog";
const router = useRouter();
const logo = ref("");
const mainNavs = ref([]);
const moreNavs = ref([]);
const curPath = ref(router.currentRoute.value.path);
const title = ref("");
const mainWinHeight = window.innerHeight - 50;
const loginUser = ref({});
const version = ref(process.env.VUE_APP_VERSION);
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const docsURL = ref(process.env.VUE_APP_DOCS_URL);
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const store = useSharedStore();
const show = ref(false);
watch(
() => store.showLoginDialog,
(newValue) => {
show.value = newValue;
}
);
// 监听路由变化
router.beforeEach((to, from, next) => {
curPath.value = to.path;
next();
});
if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url;
}
const changeNav = (item) => {
curPath.value = item.url;
if (item.url.indexOf("http") !== -1) {
// 外部链接
router.push({ name: "ExternalLink", query: { url: item.url } });
} else {
router.push(item.url);
}
};
onMounted(() => {
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
// 获取菜单
httpGet("/api/menu/list")
.then((res) => {
mainNavs.value = res.data;
// 根据窗口的高度计算应该显示多少菜单
const rows = Math.floor((window.innerHeight - 100) / 90);
if (res.data.length > rows) {
mainNavs.value = res.data.slice(0, rows);
moreNavs.value = res.data.slice(rows);
}
})
.catch((e) => {
ElMessage.error("获取系统菜单失败:" + e.message);
});
getLicenseInfo()
.then((res) => {
license.value = res.data;
})
.catch((e) => {
license.value = { de_copy: false };
showMessageError("获取 License 配置:" + e.message);
});
init();
});
const init = () => {
checkSession()
.then((user) => {
loginUser.value = user;
})
.catch(() => {});
};
const logout = function () {
httpGet("/api/user/logout")
.then(() => {
removeUserToken();
store.setShowLoginDialog(true);
store.setIsLogin(false);
loginUser.value = {};
// 刷新组件
routerViewKey.value += 1;
})
.catch(() => {
ElMessage.error("注销失败!");
});
};
const loginCallback = () => {
init();
// 刷新组件
routerViewKey.value += 1;
};
</script>
<style lang="stylus" scoped>
@import "@/assets/css/custom-scroll.styl"
@import "@/assets/css/home.styl"
</style>

View File

@@ -5,7 +5,10 @@
<div class="menu-box"> <div class="menu-box">
<el-menu mode="horizontal" :ellipsis="false"> <el-menu mode="horizontal" :ellipsis="false">
<div class="menu-item"> <div class="menu-item">
<el-image :src="logo" class="logo" alt="Geek-AI" /> <!-- <el-image :src="logo" class="logo" alt="Geek-AI" /> -->
<div class="logo-box">
<img src="@/assets/img/logo.png" alt="" />
</div>
</div> </div>
<div class="menu-item"> <div class="menu-item">
<span v-if="!license.de_copy"> <span v-if="!license.de_copy">