merge v4.1.0 and fixed conflicts
@@ -6,4 +6,6 @@ VUE_APP_ADMIN_USER=admin
 | 
			
		||||
VUE_APP_ADMIN_PASS=admin123
 | 
			
		||||
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_
 | 
			
		||||
VUE_APP_TITLE="Geek-AI 创作系统"
 | 
			
		||||
VUE_APP_VERSION=v4.0.8
 | 
			
		||||
VUE_APP_VERSION=v4.1.0
 | 
			
		||||
VUE_APP_DOCS_URL=https://docs.geekai.me
 | 
			
		||||
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai
 | 
			
		||||
 
 | 
			
		||||
@@ -2,4 +2,6 @@ VUE_APP_API_HOST=
 | 
			
		||||
VUE_APP_WS_HOST=
 | 
			
		||||
VUE_APP_KEY_PREFIX=ChatPLUS_
 | 
			
		||||
VUE_APP_TITLE="Geek-AI 创作系统"
 | 
			
		||||
VUE_APP_VERSION=v4.0.8
 | 
			
		||||
VUE_APP_VERSION=v4.1.0
 | 
			
		||||
VUE_APP_DOCS_URL=https://docs.geekai.me
 | 
			
		||||
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.1 KiB  | 
| 
		 Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 12 KiB  | 
| 
		 Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.7 KiB  | 
| 
		 Before Width: | Height: | Size: 388 KiB After Width: | Height: | Size: 388 KiB  | 
@@ -3,14 +3,12 @@
 | 
			
		||||
    display flex
 | 
			
		||||
    width 100%
 | 
			
		||||
 | 
			
		||||
    .el-input {
 | 
			
		||||
      width 50%
 | 
			
		||||
    .el-input,.el-select,.el-switch {
 | 
			
		||||
      margin-right 10px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .info {
 | 
			
		||||
      margin-left 6px
 | 
			
		||||
      margin-top 2px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,27 +2,27 @@
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout {
 | 
			
		||||
#app .chat-page {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside {
 | 
			
		||||
#app .chat-page .el-aside {
 | 
			
		||||
    background-color: #252526;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .title-box {
 | 
			
		||||
#app .chat-page .el-aside .title-box {
 | 
			
		||||
    padding: 6px 10px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .title-box span {
 | 
			
		||||
#app .chat-page .el-aside .title-box span {
 | 
			
		||||
    padding-top: 5px;
 | 
			
		||||
    padding-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list {
 | 
			
		||||
#app .chat-page .el-aside .chat-list {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-flow: column;
 | 
			
		||||
    background-color: #28292a;
 | 
			
		||||
@@ -30,28 +30,28 @@
 | 
			
		||||
    border-right: 1px solid #2f3032;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .search-box {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .search-box {
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    padding: 10px 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .search-box .el-input__wrapper {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .search-box .el-input__wrapper {
 | 
			
		||||
    background-color: #363535;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list ::-webkit-scrollbar {
 | 
			
		||||
#app .chat-page .el-aside .chat-list ::-webkit-scrollbar {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
@@ -59,17 +59,17 @@
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item:hover {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item:hover {
 | 
			
		||||
    background-color: #343540;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .avatar {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .avatar {
 | 
			
		||||
    width: 28px;
 | 
			
		||||
    height: 28px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title-input {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-title-input {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    margin-top: 4px;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
@@ -79,7 +79,7 @@
 | 
			
		||||
    width: 190px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-title {
 | 
			
		||||
    color: #c1c1c1;
 | 
			
		||||
    padding: 5px 10px;
 | 
			
		||||
    max-width: 220px;
 | 
			
		||||
@@ -89,7 +89,7 @@
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .btn {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .btn {
 | 
			
		||||
    display: none;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 2px;
 | 
			
		||||
@@ -97,19 +97,19 @@
 | 
			
		||||
    color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .btn .el-icon {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item .btn .el-icon {
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item.active {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item.active {
 | 
			
		||||
    background-color: #343540;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .chat-list .content .chat-list-item.active .btn {
 | 
			
		||||
#app .chat-page .el-aside .chat-list .content .chat-list-item.active .btn {
 | 
			
		||||
    display: inline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .tool-box {
 | 
			
		||||
#app .chat-page .el-aside .tool-box {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
@@ -117,48 +117,48 @@
 | 
			
		||||
    border-top: 1px solid #3c3c3c;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .tool-box .user-info {
 | 
			
		||||
#app .chat-page .el-aside .tool-box .user-info {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link {
 | 
			
		||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .el-image {
 | 
			
		||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link .el-image {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .username {
 | 
			
		||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link .username {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    line-height: 22px;
 | 
			
		||||
    width: 230px;
 | 
			
		||||
    padding-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .el-icon {
 | 
			
		||||
#app .chat-page .el-aside .tool-box .user-info .el-dropdown-link .el-icon {
 | 
			
		||||
    color: #ccc;
 | 
			
		||||
    line-height: 24px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main {
 | 
			
		||||
#app .chat-page .el-main {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    --el-main-padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head {
 | 
			
		||||
#app .chat-page .el-main .chat-head {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 50px;
 | 
			
		||||
    background-color: #28292a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .chat-config {
 | 
			
		||||
#app .chat-page .el-main .chat-head .chat-config {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
@@ -166,54 +166,54 @@
 | 
			
		||||
    padding-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .chat-config .role-select-label {
 | 
			
		||||
#app .chat-page .el-main .chat-head .chat-config .role-select-label {
 | 
			
		||||
    color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .chat-config .el-select {
 | 
			
		||||
#app .chat-page .el-main .chat-head .chat-config .el-select {
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .chat-config .role-select {
 | 
			
		||||
#app .chat-page .el-main .chat-head .chat-config .role-select {
 | 
			
		||||
    max-width: 130px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .chat-config .el-button .el-icon {
 | 
			
		||||
#app .chat-page .el-main .chat-head .chat-config .el-button .el-icon {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .iconfont {
 | 
			
		||||
#app .chat-page .el-main .chat-head .iconfont {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .is-circle {
 | 
			
		||||
#app .chat-page .el-main .chat-head .is-circle {
 | 
			
		||||
    margin-left: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-head .is-circle .iconfont {
 | 
			
		||||
#app .chat-page .el-main .chat-head .is-circle .iconfont {
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box {
 | 
			
		||||
#app .chat-page .el-main .chat-box {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    background-color: #fff;
 | 
			
		||||
    border-left: 1px solid #4f4f4f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container ::-webkit-scrollbar {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container ::-webkit-scrollbar {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .chat-box {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .chat-box {
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    --content-font-size: 16px;
 | 
			
		||||
    --content-color: #c1c1c1;
 | 
			
		||||
@@ -221,28 +221,28 @@
 | 
			
		||||
    padding: 0 0 50px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .chat-box .chat-line {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .chat-box .chat-line {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .re-generate {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .re-generate {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .re-generate .btn-box {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .re-generate .btn-box {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .re-generate .btn-box .el-button .el-icon {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .re-generate .btn-box .el-button .el-icon {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .input-box {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .input-box {
 | 
			
		||||
    background-color: #fff;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
@@ -251,7 +251,7 @@
 | 
			
		||||
    padding: 0 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .input-box .input-container {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .input-box .input-container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    border: none;
 | 
			
		||||
@@ -261,24 +261,24 @@
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .el-textarea .el-textarea__inner::-webkit-scrollbar {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .el-textarea .el-textarea__inner::-webkit-scrollbar {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .select-file {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .select-file {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 48px;
 | 
			
		||||
    top: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .send-btn {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .send-btn {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 12px;
 | 
			
		||||
    top: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container .input-box .input-container .send-btn .el-button {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container .input-box .input-container .send-btn .el-button {
 | 
			
		||||
    padding: 8px 5px;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    background: #19c37d;
 | 
			
		||||
@@ -286,7 +286,7 @@
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app .common-layout .el-main .chat-box #container::-webkit-scrollbar {
 | 
			
		||||
#app .chat-page .el-main .chat-box #container::-webkit-scrollbar {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ $borderColor = #4676d0;
 | 
			
		||||
 | 
			
		||||
  height: 100%;
 | 
			
		||||
 | 
			
		||||
  .common-layout {
 | 
			
		||||
  .chat-page {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
 | 
			
		||||
    // left side
 | 
			
		||||
@@ -156,6 +156,20 @@ $borderColor = #4676d0;
 | 
			
		||||
            max-width 130px;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .setting {
 | 
			
		||||
            padding 5px
 | 
			
		||||
            border-radius 5px
 | 
			
		||||
            cursor pointer
 | 
			
		||||
            .iconfont {
 | 
			
		||||
              font-size 18px
 | 
			
		||||
              color #19c37d
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &:hover {
 | 
			
		||||
              background #D5FAD3
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .el-button {
 | 
			
		||||
            .el-icon {
 | 
			
		||||
              margin-right 5px;
 | 
			
		||||
@@ -169,13 +183,25 @@ $borderColor = #4676d0;
 | 
			
		||||
          position relative
 | 
			
		||||
 | 
			
		||||
          ::-webkit-scrollbar {
 | 
			
		||||
            width: 0;
 | 
			
		||||
            height: 0;
 | 
			
		||||
            background-color: transparent;
 | 
			
		||||
            width: 12px /* 滚动条宽度 */
 | 
			
		||||
            background #F1F1F1
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ::-webkit-scrollbar-track {
 | 
			
		||||
            background-color: #e1e1e1;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ::-webkit-scrollbar-thumb {
 | 
			
		||||
            background-color: #c1c1c1;
 | 
			
		||||
            border-radius 12px
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ::-webkit-scrollbar-thumb:hover {
 | 
			
		||||
            background-color: #A8A8A8;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .chat-box {
 | 
			
		||||
            overflow-y: scroll;
 | 
			
		||||
            overflow-y: auto;
 | 
			
		||||
            //border-bottom: 1px solid #4f4f4f
 | 
			
		||||
 | 
			
		||||
            // 变量定义
 | 
			
		||||
@@ -252,25 +278,35 @@ $borderColor = #4676d0;
 | 
			
		||||
                  border: 2px solid #21AA93
 | 
			
		||||
                  border-radius 10px
 | 
			
		||||
                  padding 10px
 | 
			
		||||
                  background-color #F4F4F4
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                  .prompt-input::-webkit-scrollbar {
 | 
			
		||||
                    width: 0;
 | 
			
		||||
                    height: 0;
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  .prompt-input {
 | 
			
		||||
                  .input-inner {
 | 
			
		||||
                    display flex
 | 
			
		||||
                    flex-flow column
 | 
			
		||||
                    width 100%
 | 
			
		||||
                    line-height: 24px
 | 
			
		||||
                    border none
 | 
			
		||||
                    font-size 14px
 | 
			
		||||
                    background none
 | 
			
		||||
                    resize: none
 | 
			
		||||
                    white-space: pre-wrap; /* 保持文本换行 */
 | 
			
		||||
                    word-wrap: break-word; /* 允许单词换行 */
 | 
			
		||||
                    overflow-wrap: break-word; /* 允许长单词换行,适用于现代浏览器 */
 | 
			
		||||
 | 
			
		||||
                    .file-list {
 | 
			
		||||
                      padding-bottom 10px
 | 
			
		||||
                    }
 | 
			
		||||
                    .prompt-input::-webkit-scrollbar {
 | 
			
		||||
                      width: 0;
 | 
			
		||||
                      height: 0;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    .prompt-input {
 | 
			
		||||
                      width 100%
 | 
			
		||||
                      line-height: 24px
 | 
			
		||||
                      border none
 | 
			
		||||
                      font-size 14px
 | 
			
		||||
                      background none
 | 
			
		||||
                      resize: none
 | 
			
		||||
                      white-space: pre-wrap; /* 保持文本换行 */
 | 
			
		||||
                      word-wrap: break-word; /* 允许单词换行 */
 | 
			
		||||
                      overflow-wrap: break-word; /* 允许长单词换行,适用于现代浏览器 */
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                  .send-btn {
 | 
			
		||||
                    width 32px
 | 
			
		||||
                    margin-left 10px
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										115
									
								
								web/src/assets/css/login.styl
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,115 @@
 | 
			
		||||
.bg {
 | 
			
		||||
  position fixed
 | 
			
		||||
  left 0
 | 
			
		||||
  right 0
 | 
			
		||||
  top 0
 | 
			
		||||
  bottom 0
 | 
			
		||||
  background-color #313237
 | 
			
		||||
  background-image url("~@/assets/img/login-bg.jpg")
 | 
			
		||||
  background-size cover
 | 
			
		||||
  background-position center
 | 
			
		||||
  background-repeat repeat-y
 | 
			
		||||
  //filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
  .contain {
 | 
			
		||||
    position fixed
 | 
			
		||||
    left 50%
 | 
			
		||||
    top 40%
 | 
			
		||||
    width 90%
 | 
			
		||||
    max-width 400px;
 | 
			
		||||
    transform translate(-50%, -50%)
 | 
			
		||||
    padding 20px 10px;
 | 
			
		||||
    color #ffffff
 | 
			
		||||
    border-radius 10px;
 | 
			
		||||
 | 
			
		||||
    .logo {
 | 
			
		||||
      text-align center
 | 
			
		||||
 | 
			
		||||
      .el-image {
 | 
			
		||||
        width 120px;
 | 
			
		||||
        cursor pointer
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .header {
 | 
			
		||||
      width 100%
 | 
			
		||||
      margin-bottom 24px
 | 
			
		||||
      font-size 24px
 | 
			
		||||
      color $white_v1
 | 
			
		||||
      letter-space 2px
 | 
			
		||||
      text-align center
 | 
			
		||||
      padding-top 10px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .content {
 | 
			
		||||
      width 100%
 | 
			
		||||
      height: auto
 | 
			
		||||
      border-radius 3px
 | 
			
		||||
 | 
			
		||||
      .block {
 | 
			
		||||
        margin-bottom 16px
 | 
			
		||||
 | 
			
		||||
        .el-input__inner {
 | 
			
		||||
          border 1px solid $gray-v6 !important
 | 
			
		||||
 | 
			
		||||
          .el-icon-user, .el-icon-lock {
 | 
			
		||||
            font-size 20px
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .btn-row {
 | 
			
		||||
        padding-top 10px;
 | 
			
		||||
 | 
			
		||||
        .login-btn {
 | 
			
		||||
          width 100%
 | 
			
		||||
          font-size 16px
 | 
			
		||||
          letter-spacing 2px
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .text-line {
 | 
			
		||||
        justify-content center
 | 
			
		||||
        padding-top 10px;
 | 
			
		||||
        font-size 14px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .opt {
 | 
			
		||||
        padding 15px
 | 
			
		||||
        .el-col {
 | 
			
		||||
          text-align center
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .divider {
 | 
			
		||||
        border-top: 2px solid #c1c1c1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .clogin {
 | 
			
		||||
        padding 15px
 | 
			
		||||
        display flex
 | 
			
		||||
        justify-content center
 | 
			
		||||
 | 
			
		||||
        .iconfont {
 | 
			
		||||
          font-size 20px
 | 
			
		||||
          background: #E9F1F6;
 | 
			
		||||
          padding: 8px;
 | 
			
		||||
          border-radius: 50%;
 | 
			
		||||
        }
 | 
			
		||||
        .iconfont.icon-wechat {
 | 
			
		||||
          color #0bc15f
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .footer {
 | 
			
		||||
    color #ffffff;
 | 
			
		||||
 | 
			
		||||
    .container {
 | 
			
		||||
      padding 20px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										237
									
								
								web/src/assets/css/markdown/vue.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,237 @@
 | 
			
		||||
.chat-line {
 | 
			
		||||
    ol, ul {
 | 
			
		||||
        margin: 0.8em 0;
 | 
			
		||||
        list-style: normal;
 | 
			
		||||
    }
 | 
			
		||||
    a {
 | 
			
		||||
        color: #42b983;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        padding: 0 2px;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1,
 | 
			
		||||
    h2,
 | 
			
		||||
    h3,
 | 
			
		||||
    h4,
 | 
			
		||||
    h5,
 | 
			
		||||
    h6 {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        margin-top: 1rem;
 | 
			
		||||
        margin-bottom: 1rem;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        line-height: 1.4;
 | 
			
		||||
        cursor: text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1:hover a.anchor,
 | 
			
		||||
    h2:hover a.anchor,
 | 
			
		||||
    h3:hover a.anchor,
 | 
			
		||||
    h4:hover a.anchor,
 | 
			
		||||
    h5:hover a.anchor,
 | 
			
		||||
    h6:hover a.anchor {
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1 tt,
 | 
			
		||||
    h1 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h2 tt,
 | 
			
		||||
    h2 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h3 tt,
 | 
			
		||||
    h3 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h4 tt,
 | 
			
		||||
    h4 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h5 tt,
 | 
			
		||||
    h5 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h6 tt,
 | 
			
		||||
    h6 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h2 a,
 | 
			
		||||
    h3 a {
 | 
			
		||||
        color: #34495e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1 {
 | 
			
		||||
        padding-bottom: .4rem;
 | 
			
		||||
        font-size: 2.2rem;
 | 
			
		||||
        line-height: 1.3;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h2 {
 | 
			
		||||
        font-size: 1.75rem;
 | 
			
		||||
        line-height: 1.225;
 | 
			
		||||
        margin: 35px 0 15px;
 | 
			
		||||
        padding-bottom: 0.5em;
 | 
			
		||||
        border-bottom: 1px solid #ddd;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h3 {
 | 
			
		||||
        font-size: 1.4rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        margin: 20px 0 7px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h4 {
 | 
			
		||||
        font-size: 1.2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h5 {
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h6 {
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        color: #777;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p,
 | 
			
		||||
    blockquote,
 | 
			
		||||
    ul,
 | 
			
		||||
    ol,
 | 
			
		||||
    dl,
 | 
			
		||||
    table {
 | 
			
		||||
        margin: 0.8em 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    li > ol,
 | 
			
		||||
    li > ul {
 | 
			
		||||
        margin: 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hr {
 | 
			
		||||
        height: 2px;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        margin: 16px 0;
 | 
			
		||||
        background-color: #e7e7e7;
 | 
			
		||||
        border: 0 none;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        box-sizing: content-box;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h2:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h1:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h1:first-child + h2 {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h3:first-child,
 | 
			
		||||
    body > h4:first-child,
 | 
			
		||||
    body > h5:first-child,
 | 
			
		||||
    body > h6:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a:first-child h1,
 | 
			
		||||
    a:first-child h2,
 | 
			
		||||
    a:first-child h3,
 | 
			
		||||
    a:first-child h4,
 | 
			
		||||
    a:first-child h5,
 | 
			
		||||
    a:first-child h6 {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1 p,
 | 
			
		||||
    h2 p,
 | 
			
		||||
    h3 p,
 | 
			
		||||
    h4 p,
 | 
			
		||||
    h5 p,
 | 
			
		||||
    h6 p {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    li p.first {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul,
 | 
			
		||||
    ol {
 | 
			
		||||
        padding-left: 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul:first-child,
 | 
			
		||||
    ol:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul:last-child,
 | 
			
		||||
    ol:last-child {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    blockquote {
 | 
			
		||||
        border-left: 4px solid #42b983;
 | 
			
		||||
        padding: 10px 15px;
 | 
			
		||||
        color: #777;
 | 
			
		||||
        background-color: rgba(66, 185, 131, .1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        word-break: initial;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr {
 | 
			
		||||
        border-top: 1px solid #dfe2e5;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr:nth-child(2n),
 | 
			
		||||
    thead {
 | 
			
		||||
        background-color: #fafafa;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr th {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        border: 1px solid #dfe2e5;
 | 
			
		||||
        border-bottom: 0;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 6px 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr td {
 | 
			
		||||
        border: 1px solid #dfe2e5;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 6px 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr th:first-child,
 | 
			
		||||
    table tr td:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr th:last-child,
 | 
			
		||||
    table tr td:last-child {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.1 MiB  | 
@@ -1,152 +1,420 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="chat-line chat-line-prompt">
 | 
			
		||||
  <div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
 | 
			
		||||
    <div class="chat-line-inner">
 | 
			
		||||
        <div class="chat-icon">
 | 
			
		||||
          <img :src="data.icon" alt="User"/>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="chat-item">
 | 
			
		||||
          <div v-if="files.length > 0" class="file-list-box">
 | 
			
		||||
            <div v-for="file in files">
 | 
			
		||||
              <div class="image" v-if="isImage(file.ext)">
 | 
			
		||||
                <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 class="body">
 | 
			
		||||
                  <div class="title">
 | 
			
		||||
                    <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="info">
 | 
			
		||||
                    <span>{{GetFileType(file.ext)}}</span>
 | 
			
		||||
                    <span>{{FormatFileSize(file.size)}}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="content" v-html="content"></div>
 | 
			
		||||
          <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">tokens: {{ finalTokens }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="chat-line chat-line-prompt-chat" v-else>
 | 
			
		||||
    <div class="chat-line-inner">
 | 
			
		||||
      <div class="chat-icon">
 | 
			
		||||
        <img :src="icon" alt="User"/>
 | 
			
		||||
        <img :src="data.icon" alt="User"/>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="chat-item">
 | 
			
		||||
        <div class="content" v-html="content"></div>
 | 
			
		||||
        <div class="bar" v-if="createdAt">
 | 
			
		||||
          <span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
 | 
			
		||||
          <!--          <span class="bar-item">Tokens: {{ finalTokens }}</span>-->
 | 
			
		||||
 | 
			
		||||
        <div v-if="files.length > 0" class="file-list-box">
 | 
			
		||||
          <div v-for="file in files">
 | 
			
		||||
            <div class="image" v-if="isImage(file.ext)">
 | 
			
		||||
              <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 class="body">
 | 
			
		||||
                <div class="title">
 | 
			
		||||
                  <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="info">
 | 
			
		||||
                  <span>{{GetFileType(file.ext)}}</span>
 | 
			
		||||
                  <span>{{FormatFileSize(file.size)}}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="content-wrapper">
 | 
			
		||||
          <div class="content" v-html="content"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <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">tokens: {{ finalTokens }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {defineComponent} from "vue"
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref} from "vue"
 | 
			
		||||
import {Clock} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpPost} from "@/utils/http";
 | 
			
		||||
import hl from "highlight.js";
 | 
			
		||||
import {dateFormat, isImage, processPrompt} from "@/utils/libs";
 | 
			
		||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'ChatPrompt',
 | 
			
		||||
  components: {Clock},
 | 
			
		||||
  methods: {},
 | 
			
		||||
  props: {
 | 
			
		||||
    content: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    icon: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'images/user-icon.png',
 | 
			
		||||
    },
 | 
			
		||||
    createdAt: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    tokens: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 0,
 | 
			
		||||
    },
 | 
			
		||||
    model: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      finalTokens: this.tokens
 | 
			
		||||
const mathjaxPlugin = require('markdown-it-mathjax3')
 | 
			
		||||
const md = require('markdown-it')({
 | 
			
		||||
  breaks: true,
 | 
			
		||||
  html: true,
 | 
			
		||||
  linkify: true,
 | 
			
		||||
  typographer: true,
 | 
			
		||||
  highlight: function (str, lang) {
 | 
			
		||||
    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>
 | 
			
		||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '</textarea>')}</textarea>`
 | 
			
		||||
    if (lang && hl.getLanguage(lang)) {
 | 
			
		||||
      const langHtml = `<span class="lang-name">${lang}</span>`
 | 
			
		||||
      // 处理代码高亮
 | 
			
		||||
      const preCode = hl.highlight(lang, str, true).value
 | 
			
		||||
      // 将代码包裹在 pre 中
 | 
			
		||||
      return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 处理代码高亮
 | 
			
		||||
    const preCode = md.utils.escapeHtml(str)
 | 
			
		||||
    // 将代码包裹在 pre 中
 | 
			
		||||
    return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
md.use(mathjaxPlugin)
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  data: {
 | 
			
		||||
    type: Object,
 | 
			
		||||
    default: {
 | 
			
		||||
      content: '',
 | 
			
		||||
      created_at: '',
 | 
			
		||||
      tokens: 0,
 | 
			
		||||
      model: '',
 | 
			
		||||
      icon: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    if (!this.finalTokens) {
 | 
			
		||||
      httpPost("/api/chat/tokens", {text: this.content, model: this.model}).then(res => {
 | 
			
		||||
        this.finalTokens = res.data;
 | 
			
		||||
      }).catch(() => {
 | 
			
		||||
      })
 | 
			
		||||
  listStyle: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: 'list',
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
const finalTokens = ref(props.data.tokens)
 | 
			
		||||
const content =ref(processPrompt(props.data.content))
 | 
			
		||||
const files = ref([])
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (!finalTokens.value) {
 | 
			
		||||
    httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
 | 
			
		||||
      finalTokens.value = res.data;
 | 
			
		||||
    }).catch(() => {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const linkRegex = /(https?:\/\/\S+)/g;
 | 
			
		||||
  const links = props.data.content.match(linkRegex);
 | 
			
		||||
  if (links) {
 | 
			
		||||
    httpPost("/api/upload/list", {urls: links}).then(res => {
 | 
			
		||||
      files.value = res.data
 | 
			
		||||
    }).catch(() => {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    for (let link of links) {
 | 
			
		||||
      content.value = content.value.replace(link,"")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  content.value = md.render(content.value.trim())
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
.chat-line-prompt {
 | 
			
		||||
  background-color #ffffff;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width 100%
 | 
			
		||||
  padding-bottom: 1.5rem;
 | 
			
		||||
  padding-top: 1.5rem;
 | 
			
		||||
  border-bottom: 1px solid #d9d9e3;
 | 
			
		||||
@import '@/assets/css/markdown/vue.css';
 | 
			
		||||
.chat-page,.chat-export {
 | 
			
		||||
  .chat-line-prompt-list {
 | 
			
		||||
    background-color #ffffff;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width 100%
 | 
			
		||||
    padding-bottom: 1.5rem;
 | 
			
		||||
    padding-top: 1.5rem;
 | 
			
		||||
    border-bottom: 1px solid #d9d9e3;
 | 
			
		||||
 | 
			
		||||
  .chat-line-inner {
 | 
			
		||||
    display flex;
 | 
			
		||||
    width 100%;
 | 
			
		||||
    max-width 900px;
 | 
			
		||||
    padding-left 10px;
 | 
			
		||||
    .chat-line-inner {
 | 
			
		||||
      display flex;
 | 
			
		||||
      width 100%;
 | 
			
		||||
      max-width 900px;
 | 
			
		||||
      padding-left 10px;
 | 
			
		||||
 | 
			
		||||
    .chat-icon {
 | 
			
		||||
      margin-right 20px;
 | 
			
		||||
 | 
			
		||||
      img {
 | 
			
		||||
        width: 36px;
 | 
			
		||||
        height: 36px;
 | 
			
		||||
        border-radius: 10px;
 | 
			
		||||
        padding: 1px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .chat-item {
 | 
			
		||||
      width 100%
 | 
			
		||||
      position: relative;
 | 
			
		||||
      padding: 0 5px 0 0;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
 | 
			
		||||
      .content {
 | 
			
		||||
        word-break break-word;
 | 
			
		||||
        padding: 6px 10px;
 | 
			
		||||
        color #374151;
 | 
			
		||||
        font-size: var(--content-font-size);
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
      .chat-icon {
 | 
			
		||||
        margin-right 20px;
 | 
			
		||||
 | 
			
		||||
        img {
 | 
			
		||||
          max-width: 600px;
 | 
			
		||||
          width: 36px;
 | 
			
		||||
          height: 36px;
 | 
			
		||||
          border-radius: 10px;
 | 
			
		||||
          margin 10px 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        a {
 | 
			
		||||
          color #20a0ff
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        p {
 | 
			
		||||
          line-height 1.5
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        p:last-child {
 | 
			
		||||
          margin-bottom: 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        p:first-child {
 | 
			
		||||
          margin-top 0
 | 
			
		||||
          padding: 1px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .bar {
 | 
			
		||||
        padding 10px;
 | 
			
		||||
      .chat-item {
 | 
			
		||||
        width 100%
 | 
			
		||||
        padding: 0 5px 0 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
 | 
			
		||||
        .bar-item {
 | 
			
		||||
          background-color #f7f7f8;
 | 
			
		||||
          color #888
 | 
			
		||||
          padding 3px 5px;
 | 
			
		||||
          margin-right 10px;
 | 
			
		||||
          border-radius 5px;
 | 
			
		||||
 | 
			
		||||
          .el-icon {
 | 
			
		||||
        .file-list-box {
 | 
			
		||||
          display flex
 | 
			
		||||
          flex-flow column
 | 
			
		||||
          .image {
 | 
			
		||||
            display flex
 | 
			
		||||
            flex-flow row
 | 
			
		||||
            margin-right 10px
 | 
			
		||||
            position relative
 | 
			
		||||
            top 2px;
 | 
			
		||||
 | 
			
		||||
            .el-image {
 | 
			
		||||
              border 1px solid #e3e3e3
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              margin-bottom 10px
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          .item {
 | 
			
		||||
            display flex
 | 
			
		||||
            flex-flow row
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
            background-color #ffffff
 | 
			
		||||
            border 1px solid #e3e3e3
 | 
			
		||||
            padding 6px
 | 
			
		||||
            margin-bottom 10px
 | 
			
		||||
 | 
			
		||||
            .icon {
 | 
			
		||||
              .el-image {
 | 
			
		||||
                width 40px
 | 
			
		||||
                height 40px
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            .body {
 | 
			
		||||
              margin-left 8px
 | 
			
		||||
              font-size 14px
 | 
			
		||||
              .title {
 | 
			
		||||
                font-weight bold
 | 
			
		||||
                line-height 24px
 | 
			
		||||
                color #0D0D0D
 | 
			
		||||
              }
 | 
			
		||||
              .info {
 | 
			
		||||
                color #B4B4B4
 | 
			
		||||
 | 
			
		||||
                span {
 | 
			
		||||
                  margin-right 10px
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .content {
 | 
			
		||||
          word-break break-word;
 | 
			
		||||
          padding: 0;
 | 
			
		||||
          color #374151;
 | 
			
		||||
          font-size: var(--content-font-size);
 | 
			
		||||
          border-radius: 5px;
 | 
			
		||||
          overflow: auto;
 | 
			
		||||
 | 
			
		||||
          img {
 | 
			
		||||
            max-width: 600px;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            margin 10px 0
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          p {
 | 
			
		||||
            line-height 1.5
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          p:last-child {
 | 
			
		||||
            margin-bottom: 0
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          p:first-child {
 | 
			
		||||
            margin-top 0
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .bar {
 | 
			
		||||
          padding 10px 10px 10px 0;
 | 
			
		||||
 | 
			
		||||
          .bar-item {
 | 
			
		||||
            background-color #f7f7f8;
 | 
			
		||||
            color #888
 | 
			
		||||
            padding 3px 5px;
 | 
			
		||||
            margin-right 10px;
 | 
			
		||||
            border-radius 5px;
 | 
			
		||||
 | 
			
		||||
            .el-icon {
 | 
			
		||||
              position relative
 | 
			
		||||
              top 2px;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .chat-line-prompt-chat {
 | 
			
		||||
    background-color #ffffff;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width 100%
 | 
			
		||||
    padding-bottom: 1.5rem;
 | 
			
		||||
    padding-top: 1.5rem;
 | 
			
		||||
 | 
			
		||||
    .chat-line-inner {
 | 
			
		||||
      display flex;
 | 
			
		||||
      width 100%;
 | 
			
		||||
      padding 0 25px;
 | 
			
		||||
 | 
			
		||||
      .chat-icon {
 | 
			
		||||
        margin-right 20px;
 | 
			
		||||
 | 
			
		||||
        img {
 | 
			
		||||
          width: 36px;
 | 
			
		||||
          height: 36px;
 | 
			
		||||
          border-radius: 50%;
 | 
			
		||||
          padding: 1px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .chat-item {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        max-width 60%
 | 
			
		||||
 | 
			
		||||
        .file-list-box {
 | 
			
		||||
          display flex
 | 
			
		||||
          flex-flow column
 | 
			
		||||
          .image {
 | 
			
		||||
            display flex
 | 
			
		||||
            flex-flow row
 | 
			
		||||
            margin-right 10px
 | 
			
		||||
            position relative
 | 
			
		||||
 | 
			
		||||
            .el-image {
 | 
			
		||||
              border 1px solid #e3e3e3
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              margin-bottom 10px
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          .item {
 | 
			
		||||
            display flex
 | 
			
		||||
            flex-flow row
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
            background-color #ffffff
 | 
			
		||||
            border 1px solid #e3e3e3
 | 
			
		||||
            padding 6px
 | 
			
		||||
            margin-bottom 10px
 | 
			
		||||
 | 
			
		||||
            .icon {
 | 
			
		||||
              .el-image {
 | 
			
		||||
                width 40px
 | 
			
		||||
                height 40px
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            .body {
 | 
			
		||||
              margin-left 8px
 | 
			
		||||
              font-size 14px
 | 
			
		||||
              .title {
 | 
			
		||||
                font-weight bold
 | 
			
		||||
                line-height 24px
 | 
			
		||||
                color #0D0D0D
 | 
			
		||||
              }
 | 
			
		||||
              .info {
 | 
			
		||||
                color #B4B4B4
 | 
			
		||||
 | 
			
		||||
                span {
 | 
			
		||||
                  margin-right 10px
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        .content-wrapper {
 | 
			
		||||
          display flex
 | 
			
		||||
          .content {
 | 
			
		||||
              word-break break-word;
 | 
			
		||||
              padding: 1rem
 | 
			
		||||
              color #222222;
 | 
			
		||||
              font-size: var(--content-font-size);
 | 
			
		||||
              overflow: auto;
 | 
			
		||||
              background-color #98e165
 | 
			
		||||
              border-radius: 0 10px 10px 10px;
 | 
			
		||||
 | 
			
		||||
              img {
 | 
			
		||||
                max-width: 600px;
 | 
			
		||||
                border-radius: 10px;
 | 
			
		||||
                margin 10px 0
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              p {
 | 
			
		||||
                line-height 1.5
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              p:last-child {
 | 
			
		||||
                margin-bottom: 0
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              p:first-child {
 | 
			
		||||
                margin-top 0
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        .bar {
 | 
			
		||||
          padding 10px 10px 10px 0;
 | 
			
		||||
 | 
			
		||||
          .bar-item {
 | 
			
		||||
            color #888
 | 
			
		||||
            padding 3px 5px;
 | 
			
		||||
            margin-right 10px;
 | 
			
		||||
            border-radius 5px;
 | 
			
		||||
 | 
			
		||||
            .el-icon {
 | 
			
		||||
              position relative
 | 
			
		||||
              top 2px;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="chat-line chat-line-reply">
 | 
			
		||||
  <div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
 | 
			
		||||
    <div class="chat-line-inner">
 | 
			
		||||
      <div class="chat-icon">
 | 
			
		||||
        <img :src="data.icon" alt="ChatGPT">
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
        <div class="content" v-html="data.content"></div>
 | 
			
		||||
        <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">Tokens: {{ tokens }}</span>-->
 | 
			
		||||
                    <span class="bar-item">tokens: {{ data.tokens }}</span>
 | 
			
		||||
          <span class="bar-item">
 | 
			
		||||
              <el-tooltip
 | 
			
		||||
                  class="box-item"
 | 
			
		||||
@@ -61,6 +61,59 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="chat-line chat-line-reply-chat" v-else>
 | 
			
		||||
    <div class="chat-line-inner">
 | 
			
		||||
      <div class="chat-icon">
 | 
			
		||||
        <img :src="data.icon" alt="ChatGPT">
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="chat-item">
 | 
			
		||||
        <div class="content-wrapper">
 | 
			
		||||
          <div class="content" v-html="data.content"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <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">tokens: {{ data.tokens }}</span>
 | 
			
		||||
          <span class="bar-item bg">
 | 
			
		||||
              <el-tooltip
 | 
			
		||||
                  class="box-item"
 | 
			
		||||
                  effect="dark"
 | 
			
		||||
                  content="复制回答"
 | 
			
		||||
                  placement="bottom"
 | 
			
		||||
              >
 | 
			
		||||
                <el-icon class="copy-reply" :data-clipboard-text="data.orgContent">
 | 
			
		||||
                  <DocumentCopy/>
 | 
			
		||||
                </el-icon>
 | 
			
		||||
              </el-tooltip>
 | 
			
		||||
            </span>
 | 
			
		||||
          <span v-if="!readOnly">
 | 
			
		||||
            <span class="bar-item bg" @click="reGenerate(data.prompt)">
 | 
			
		||||
            <el-tooltip
 | 
			
		||||
                class="box-item"
 | 
			
		||||
                effect="dark"
 | 
			
		||||
                content="重新生成"
 | 
			
		||||
                placement="bottom"
 | 
			
		||||
            >
 | 
			
		||||
              <el-icon><Refresh/></el-icon>
 | 
			
		||||
            </el-tooltip>
 | 
			
		||||
          </span>
 | 
			
		||||
 | 
			
		||||
          <span class="bar-item bg" @click="synthesis(data.orgContent)">
 | 
			
		||||
            <el-tooltip
 | 
			
		||||
                class="box-item"
 | 
			
		||||
                effect="dark"
 | 
			
		||||
                content="生成语音朗读"
 | 
			
		||||
                placement="bottom"
 | 
			
		||||
            >
 | 
			
		||||
              <i class="iconfont icon-speaker"></i>
 | 
			
		||||
            </el-tooltip>
 | 
			
		||||
          </span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
@@ -71,12 +124,22 @@ import {dateFormat} from "@/utils/libs";
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  data: {
 | 
			
		||||
    type: Object,
 | 
			
		||||
    default: {},
 | 
			
		||||
    default: {
 | 
			
		||||
      icon: "",
 | 
			
		||||
      content: "",
 | 
			
		||||
      created_at: "",
 | 
			
		||||
      tokens: 0,
 | 
			
		||||
      orgContent: ""
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  readOnly: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  listStyle: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: 'list',
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['regen']);
 | 
			
		||||
@@ -92,13 +155,15 @@ const synthesis = (text) => {
 | 
			
		||||
 | 
			
		||||
// 重新生成
 | 
			
		||||
const reGenerate = (prompt) => {
 | 
			
		||||
  console.log(prompt)
 | 
			
		||||
  emits('regen', prompt)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
.common-layout {
 | 
			
		||||
  .chat-line-reply {
 | 
			
		||||
@import '@/assets/css/markdown/vue.css';
 | 
			
		||||
.chat-page,.chat-export {
 | 
			
		||||
  .chat-line-reply-list {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    background-color: rgba(247, 247, 248, 1);
 | 
			
		||||
    width 100%
 | 
			
		||||
@@ -126,24 +191,18 @@ const reGenerate = (prompt) => {
 | 
			
		||||
      .chat-item {
 | 
			
		||||
        width 100%
 | 
			
		||||
        position: relative;
 | 
			
		||||
        padding: 0 0 0 5px;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
 | 
			
		||||
        .content {
 | 
			
		||||
          min-height 20px;
 | 
			
		||||
          word-break break-word;
 | 
			
		||||
          padding: 6px 10px;
 | 
			
		||||
          padding: 0
 | 
			
		||||
          color #374151;
 | 
			
		||||
          font-size: var(--content-font-size);
 | 
			
		||||
          border-radius: 5px;
 | 
			
		||||
          overflow auto;
 | 
			
		||||
 | 
			
		||||
          a {
 | 
			
		||||
            color #20a0ff
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // control the image size in content
 | 
			
		||||
 | 
			
		||||
          img {
 | 
			
		||||
            max-width: 600px;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
@@ -170,10 +229,11 @@ const reGenerate = (prompt) => {
 | 
			
		||||
 | 
			
		||||
          .code-container {
 | 
			
		||||
            position relative
 | 
			
		||||
            display flex
 | 
			
		||||
 | 
			
		||||
            .hljs {
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              line-height 1.5
 | 
			
		||||
              width 100%
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .copy-code-btn {
 | 
			
		||||
@@ -194,7 +254,7 @@ const reGenerate = (prompt) => {
 | 
			
		||||
          .lang-name {
 | 
			
		||||
            position absolute;
 | 
			
		||||
            right 10px
 | 
			
		||||
            bottom 50px
 | 
			
		||||
            bottom 20px
 | 
			
		||||
            padding 2px 6px 4px 6px
 | 
			
		||||
            background-color #444444
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
@@ -241,7 +301,7 @@ const reGenerate = (prompt) => {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        .bar {
 | 
			
		||||
          padding 10px;
 | 
			
		||||
          padding 10px 10px 10px 0;
 | 
			
		||||
 | 
			
		||||
          .bar-item {
 | 
			
		||||
            background-color #e7e7e8;
 | 
			
		||||
@@ -277,6 +337,187 @@ const reGenerate = (prompt) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .chat-line-reply-chat {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width 100%
 | 
			
		||||
    padding-bottom: 1.5rem;
 | 
			
		||||
    padding-top: 1.5rem;
 | 
			
		||||
 | 
			
		||||
    .chat-line-inner {
 | 
			
		||||
      display flex;
 | 
			
		||||
      padding 0 25px;
 | 
			
		||||
      width 100%
 | 
			
		||||
      flex-flow row-reverse
 | 
			
		||||
 | 
			
		||||
      .chat-icon {
 | 
			
		||||
        margin-left 20px;
 | 
			
		||||
 | 
			
		||||
        img {
 | 
			
		||||
          width: 36px;
 | 
			
		||||
          height: 36px;
 | 
			
		||||
          border-radius: 50%
 | 
			
		||||
          padding: 1px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .chat-item {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        max-width 60%
 | 
			
		||||
 | 
			
		||||
        .content-wrapper {
 | 
			
		||||
          display flex
 | 
			
		||||
          flex-flow row-reverse
 | 
			
		||||
          .content {
 | 
			
		||||
            min-height 20px;
 | 
			
		||||
            word-break break-word;
 | 
			
		||||
            padding: 1rem
 | 
			
		||||
            color #374151;
 | 
			
		||||
            font-size: var(--content-font-size);
 | 
			
		||||
            overflow auto;
 | 
			
		||||
            background-color #F5F5F5
 | 
			
		||||
            border-radius: 10px 0 10px 10px;
 | 
			
		||||
 | 
			
		||||
            img {
 | 
			
		||||
              max-width: 600px;
 | 
			
		||||
              border-radius: 10px;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            p {
 | 
			
		||||
              line-height 1.5
 | 
			
		||||
 | 
			
		||||
              code {
 | 
			
		||||
                color #374151
 | 
			
		||||
                background-color #e7e7e8
 | 
			
		||||
                padding 0 3px;
 | 
			
		||||
                border-radius 5px;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            p:last-child {
 | 
			
		||||
              margin-bottom: 0
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            p:first-child {
 | 
			
		||||
              margin-top 0
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .code-container {
 | 
			
		||||
              position relative
 | 
			
		||||
              display flex
 | 
			
		||||
 | 
			
		||||
              .hljs {
 | 
			
		||||
                border-radius 10px
 | 
			
		||||
                width 100%
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              .copy-code-btn {
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                right 10px
 | 
			
		||||
                top 10px
 | 
			
		||||
                cursor pointer
 | 
			
		||||
                font-size 12px
 | 
			
		||||
                color #c1c1c1
 | 
			
		||||
 | 
			
		||||
                &:hover {
 | 
			
		||||
                  color #20a0ff
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .lang-name {
 | 
			
		||||
              position absolute;
 | 
			
		||||
              right 10px
 | 
			
		||||
              bottom 20px
 | 
			
		||||
              padding 2px 6px 4px 6px
 | 
			
		||||
              background-color #444444
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              color #00e0e0
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // 设置表格边框
 | 
			
		||||
 | 
			
		||||
            table {
 | 
			
		||||
              width 100%
 | 
			
		||||
              margin-bottom 1rem
 | 
			
		||||
              color #212529
 | 
			
		||||
              border-collapse collapse;
 | 
			
		||||
              border 1px solid #dee2e6;
 | 
			
		||||
              background-color #ffffff
 | 
			
		||||
 | 
			
		||||
              thead {
 | 
			
		||||
                th {
 | 
			
		||||
                  border 1px solid #dee2e6
 | 
			
		||||
                  vertical-align: bottom
 | 
			
		||||
                  border-bottom: 2px solid #dee2e6
 | 
			
		||||
                  padding 10px
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              td {
 | 
			
		||||
                border 1px solid #dee2e6
 | 
			
		||||
                padding 10px
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 代码快
 | 
			
		||||
 | 
			
		||||
            blockquote {
 | 
			
		||||
              margin 0
 | 
			
		||||
              background-color: #ebfffe;
 | 
			
		||||
              padding: 0.8rem 1.5rem;
 | 
			
		||||
              border-left: 0.5rem solid;
 | 
			
		||||
              border-color: #026863;
 | 
			
		||||
              color: #2c3e50;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .bar {
 | 
			
		||||
          padding 10px 10px 10px 0;
 | 
			
		||||
 | 
			
		||||
          .bar-item {
 | 
			
		||||
            color #888
 | 
			
		||||
            padding 3px 5px;
 | 
			
		||||
            margin-right 10px;
 | 
			
		||||
            border-radius 5px;
 | 
			
		||||
 | 
			
		||||
            .el-icon {
 | 
			
		||||
              position relative
 | 
			
		||||
              top 2px;
 | 
			
		||||
              cursor pointer
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .bar-item.bg {
 | 
			
		||||
            background-color #e7e7e8
 | 
			
		||||
            cursor pointer
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .el-button {
 | 
			
		||||
            height 20px
 | 
			
		||||
            padding 5px 2px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .tool-box {
 | 
			
		||||
        font-size 16px;
 | 
			
		||||
 | 
			
		||||
        .el-button {
 | 
			
		||||
          height 20px
 | 
			
		||||
          padding 5px 2px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								web/src/components/ChatSetting.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-dialog
 | 
			
		||||
      class="config-dialog"
 | 
			
		||||
      v-model="showDialog"
 | 
			
		||||
      :close-on-click-modal="true"
 | 
			
		||||
      :before-close="close"
 | 
			
		||||
      style="max-width: 600px"
 | 
			
		||||
      title="聊天配置"
 | 
			
		||||
  >
 | 
			
		||||
    <div class="chat-setting">
 | 
			
		||||
      <el-form :model="data" label-width="100px" label-position="left">
 | 
			
		||||
        <el-form-item label="聊天样式:">
 | 
			
		||||
          <el-radio-group v-model="data.style" @change="(val) => {store.setChatListStyle(val)}">
 | 
			
		||||
            <el-radio value="list">列表样式</el-radio>
 | 
			
		||||
            <el-radio value="chat">对话样式</el-radio>
 | 
			
		||||
          </el-radio-group>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
      </el-form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </el-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {computed, ref} from "vue"
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
const store = useSharedStore();
 | 
			
		||||
 | 
			
		||||
const data = ref({
 | 
			
		||||
  style: store.chatListStyle,
 | 
			
		||||
})
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  show: Boolean,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const showDialog = computed(() => {
 | 
			
		||||
  return props.show
 | 
			
		||||
})
 | 
			
		||||
const emits = defineEmits(['hide']);
 | 
			
		||||
const close = function () {
 | 
			
		||||
  emits('hide', false);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.chat-setting {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										115
									
								
								web/src/components/FileList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,115 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-container class="chat-file-list">
 | 
			
		||||
    <div v-for="file in fileList">
 | 
			
		||||
      <div class="image" v-if="isImage(file.ext)">
 | 
			
		||||
        <el-image :src="file.url" fit="cover"/>
 | 
			
		||||
        <div class="action">
 | 
			
		||||
          <el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item" v-else>
 | 
			
		||||
        <div class="icon">
 | 
			
		||||
          <el-image :src="GetFileIcon(file.ext)" fit="cover"  />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="body">
 | 
			
		||||
          <div class="title">
 | 
			
		||||
            <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{substr(file.name, 30)}}</el-link>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="info">
 | 
			
		||||
            <span>{{GetFileType(file.ext)}}</span>
 | 
			
		||||
            <span>{{FormatFileSize(file.size)}}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="action">
 | 
			
		||||
          <el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </el-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {CircleCloseFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {isImage, removeArrayItem, substr} from "@/utils/libs";
 | 
			
		||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  files: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    default:[],
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
const emits = defineEmits(['removeFile']);
 | 
			
		||||
const fileList = ref(props.files)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const removeFile = (file) => {
 | 
			
		||||
  fileList.value = removeArrayItem(fileList.value, file, (v1,v2) => v1.url===v2.url)
 | 
			
		||||
  emits('removeFile', file)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
 | 
			
		||||
.chat-file-list {
 | 
			
		||||
  display flex
 | 
			
		||||
  flex-flow row
 | 
			
		||||
  .image {
 | 
			
		||||
    display flex
 | 
			
		||||
    flex-flow row
 | 
			
		||||
    margin-right 10px
 | 
			
		||||
    position relative
 | 
			
		||||
 | 
			
		||||
    .el-image {
 | 
			
		||||
      height 56px
 | 
			
		||||
      width 56px
 | 
			
		||||
      border 1px solid #e3e3e3
 | 
			
		||||
      border-radius 10px
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .item {
 | 
			
		||||
    position relative
 | 
			
		||||
    display flex
 | 
			
		||||
    flex-flow row
 | 
			
		||||
    border-radius 10px
 | 
			
		||||
    background-color #ffffff
 | 
			
		||||
    border 1px solid #e3e3e3
 | 
			
		||||
    padding 6px
 | 
			
		||||
    margin-right 10px
 | 
			
		||||
 | 
			
		||||
    .icon {
 | 
			
		||||
      .el-image {
 | 
			
		||||
        width 40px
 | 
			
		||||
        height 40px
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .body {
 | 
			
		||||
      margin-left 5px
 | 
			
		||||
      font-size 14px
 | 
			
		||||
      .title {
 | 
			
		||||
        line-height 24px
 | 
			
		||||
        color #0D0D0D
 | 
			
		||||
      }
 | 
			
		||||
      .info {
 | 
			
		||||
        color #B4B4B4
 | 
			
		||||
 | 
			
		||||
        span {
 | 
			
		||||
          margin-right 10px
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .action {
 | 
			
		||||
    position absolute
 | 
			
		||||
    top -8px
 | 
			
		||||
    right -8px
 | 
			
		||||
    color #da0d54
 | 
			
		||||
    cursor pointer
 | 
			
		||||
    font-size 20px
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-container class="file-list-box">
 | 
			
		||||
  <el-container class="file-select-box">
 | 
			
		||||
    <a class="file-upload-img" @click="fetchFiles">
 | 
			
		||||
      <i class="iconfont icon-attachment-st"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
@@ -20,6 +20,7 @@
 | 
			
		||||
                  :auto-upload="true"
 | 
			
		||||
                  :show-file-list="false"
 | 
			
		||||
                  :http-request="afterRead"
 | 
			
		||||
                  accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf"
 | 
			
		||||
              >
 | 
			
		||||
                <el-icon class="avatar-uploader-icon">
 | 
			
		||||
                  <Plus/>
 | 
			
		||||
@@ -34,8 +35,8 @@
 | 
			
		||||
                  effect="dark"
 | 
			
		||||
                  :content="file.name"
 | 
			
		||||
                  placement="top">
 | 
			
		||||
                <el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file.url)"/>
 | 
			
		||||
                <el-image :src="getFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file.url)"/>
 | 
			
		||||
                <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>
 | 
			
		||||
 | 
			
		||||
              <div class="opt">
 | 
			
		||||
@@ -55,6 +56,7 @@ import {ElMessage} from "element-plus";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {Delete, Plus} from "@element-plus/icons-vue";
 | 
			
		||||
import {isImage, removeArrayItem} from "@/utils/libs";
 | 
			
		||||
import {GetFileIcon} from "@/store/system";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  userId: Number,
 | 
			
		||||
@@ -65,30 +67,12 @@ const fileList = ref([])
 | 
			
		||||
 | 
			
		||||
const fetchFiles = () => {
 | 
			
		||||
  show.value = true
 | 
			
		||||
  httpGet("/api/upload/list").then(res => {
 | 
			
		||||
  httpPost("/api/upload/list").then(res => {
 | 
			
		||||
    fileList.value = res.data
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getFileIcon = (ext) => {
 | 
			
		||||
  const files = {
 | 
			
		||||
    ".docx": "doc.png",
 | 
			
		||||
    ".doc": "doc.png",
 | 
			
		||||
    ".xls": "xls.png",
 | 
			
		||||
    ".xlsx": "xls.png",
 | 
			
		||||
    ".ppt": "ppt.png",
 | 
			
		||||
    ".pptx": "ppt.png",
 | 
			
		||||
    ".md": "md.png",
 | 
			
		||||
    ".pdf": "pdf.png",
 | 
			
		||||
    ".sql": "sql.png"
 | 
			
		||||
  }
 | 
			
		||||
  if (files[ext]) {
 | 
			
		||||
    return '/images/ext/' + files[ext]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return '/images/ext/file.png'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const afterRead = (file) => {
 | 
			
		||||
  const formData = new FormData();
 | 
			
		||||
@@ -113,19 +97,19 @@ const removeFile = (file) => {
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const insertURL = (url) => {
 | 
			
		||||
const insertURL = (file) => {
 | 
			
		||||
  show.value = false
 | 
			
		||||
  // 如果是相对路径,处理成绝对路径
 | 
			
		||||
  if (url.indexOf("http") === -1) {
 | 
			
		||||
    url = location.protocol + "//" + location.host + url
 | 
			
		||||
  if (file.url.indexOf("http") === -1) {
 | 
			
		||||
    file.url = location.protocol + "//" + location.host + file.url
 | 
			
		||||
  }
 | 
			
		||||
  emits('selected', url)
 | 
			
		||||
  emits('selected', file)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
 | 
			
		||||
.file-list-box {
 | 
			
		||||
.file-select-box {
 | 
			
		||||
  .file-upload-img {
 | 
			
		||||
    .iconfont {
 | 
			
		||||
      font-size: 24px;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  <div class="foot-container">
 | 
			
		||||
    <div class="footer">
 | 
			
		||||
      Powered by {{ author }} @
 | 
			
		||||
      <el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank" style="--el-link-text-color:#ffffff">
 | 
			
		||||
      <el-link type="primary" :href="gitURL" target="_blank" style="--el-link-text-color:#ffffff">
 | 
			
		||||
        {{ title }} -
 | 
			
		||||
        {{ version }}
 | 
			
		||||
      </el-link>
 | 
			
		||||
@@ -15,6 +15,7 @@ import {ref} from "vue";
 | 
			
		||||
 | 
			
		||||
const title = ref(process.env.VUE_APP_TITLE)
 | 
			
		||||
const version = ref(process.env.VUE_APP_VERSION)
 | 
			
		||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
 | 
			
		||||
const author = ref('极客学长')
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -33,6 +34,10 @@ const author = ref('极客学长')
 | 
			
		||||
    font-size 14px;
 | 
			
		||||
    padding 20px;
 | 
			
		||||
    width 100%
 | 
			
		||||
 | 
			
		||||
    .el-link {
 | 
			
		||||
      color #409eff
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -289,7 +289,7 @@ const submitLogin = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  httpPost('/api/user/login', data.value).then((res) => {
 | 
			
		||||
    setUserToken(res.data)
 | 
			
		||||
    setUserToken(res.data.token)
 | 
			
		||||
    ElMessage.success("登录成功!")
 | 
			
		||||
    emits("hide")
 | 
			
		||||
    emits('success')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								web/src/components/TaskList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,58 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="running-job-list">
 | 
			
		||||
    <div class="running-job-box" v-if="list.length > 0">
 | 
			
		||||
      <div class="job-item" v-for="item in list">
 | 
			
		||||
        <div v-if="item.progress > 0" class="job-item-inner">
 | 
			
		||||
          <el-image v-if="item.img_url" :src="item['img_url']" fit="cover" loading="lazy">
 | 
			
		||||
            <template #placeholder>
 | 
			
		||||
              <div class="image-slot">
 | 
			
		||||
                正在加载图片
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #error>
 | 
			
		||||
              <div class="image-slot">
 | 
			
		||||
                <el-icon>
 | 
			
		||||
                  <Picture/>
 | 
			
		||||
                </el-icon>
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
          </el-image>
 | 
			
		||||
 | 
			
		||||
          <div class="progress">
 | 
			
		||||
            <el-progress type="circle" :percentage="item.progress" :width="100"
 | 
			
		||||
                         color="#47fff1"/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <el-image fit="cover" v-else>
 | 
			
		||||
          <template #error>
 | 
			
		||||
            <div class="image-slot">
 | 
			
		||||
              <i class="iconfont icon-quick-start"></i>
 | 
			
		||||
              <span>任务正在排队中</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-image>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <el-empty :image-size="100" v-else/>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {CircleCloseFilled, Picture} from "@element-plus/icons-vue";
 | 
			
		||||
import {isImage, removeArrayItem, substr} from "@/utils/libs";
 | 
			
		||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  list: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    default:[],
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
@import "~@/assets/css/running-job-list.styl"
 | 
			
		||||
</style>
 | 
			
		||||
@@ -46,8 +46,8 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref, watch} from "vue";
 | 
			
		||||
import {httpPost} from "@/utils/http";
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {httpGet} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {dateFormat} from "@/utils/libs";
 | 
			
		||||
import {DocumentCopy} from "@element-plus/icons-vue";
 | 
			
		||||
@@ -73,7 +73,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
// 获取数据
 | 
			
		||||
const fetchData = () => {
 | 
			
		||||
  httpPost('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => {
 | 
			
		||||
  httpGet('/api/order/list', {page: page.value, page_size: pageSize.value}).then((res) => {
 | 
			
		||||
    if (res.data) {
 | 
			
		||||
      items.value = res.data.items
 | 
			
		||||
      total.value = res.data.total
 | 
			
		||||
 
 | 
			
		||||
@@ -101,6 +101,12 @@ const routes = [
 | 
			
		||||
        meta: {title: '用户登录'},
 | 
			
		||||
        component: () => import('@/views/Login.vue'),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'login-callback',
 | 
			
		||||
        path: '/login/callback',
 | 
			
		||||
        meta: {title: '用户登录'},
 | 
			
		||||
        component: () => import('@/views/LoginCallback.vue'),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'register',
 | 
			
		||||
        path: '/register',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,19 @@
 | 
			
		||||
import {defineStore} from 'pinia';
 | 
			
		||||
import Storage from 'good-storage'
 | 
			
		||||
 | 
			
		||||
export const useSharedStore = defineStore('shared', {
 | 
			
		||||
    state: () => ({
 | 
			
		||||
        showLoginDialog: false
 | 
			
		||||
        showLoginDialog: false,
 | 
			
		||||
        chatListStyle: Storage.get("chat_list_style","chat")
 | 
			
		||||
    }),
 | 
			
		||||
    getters: {},
 | 
			
		||||
    actions: {
 | 
			
		||||
        setShowLoginDialog(value) {
 | 
			
		||||
            this.showLoginDialog = value;
 | 
			
		||||
        },
 | 
			
		||||
        setChatListStyle(value) {
 | 
			
		||||
            this.chatListStyle = value;
 | 
			
		||||
            Storage.set("chat_list_style", value);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -24,4 +24,38 @@ export function getAdminTheme() {
 | 
			
		||||
 | 
			
		||||
export function setAdminTheme(theme) {
 | 
			
		||||
    Storage.set(ADMIN_THEME, theme)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function GetFileIcon(ext) {
 | 
			
		||||
    const files = {
 | 
			
		||||
        ".docx": "doc.png",
 | 
			
		||||
        ".doc": "doc.png",
 | 
			
		||||
        ".xls": "xls.png",
 | 
			
		||||
        ".xlsx": "xls.png",
 | 
			
		||||
        ".csv": "xls.png",
 | 
			
		||||
        ".ppt": "ppt.png",
 | 
			
		||||
        ".pptx": "ppt.png",
 | 
			
		||||
        ".md": "md.png",
 | 
			
		||||
        ".pdf": "pdf.png",
 | 
			
		||||
        ".sql": "sql.png"
 | 
			
		||||
    }
 | 
			
		||||
    if (files[ext]) {
 | 
			
		||||
        return '/images/ext/' + files[ext]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return '/images/ext/file.png'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取文件类型
 | 
			
		||||
export function GetFileType (ext) {
 | 
			
		||||
    return ext.replace(".", "").toUpperCase()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 将文件大小转成字符
 | 
			
		||||
export function FormatFileSize(bytes) {
 | 
			
		||||
    if (bytes === 0) return '0 Bytes';
 | 
			
		||||
    const k = 1024;
 | 
			
		||||
    const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
 | 
			
		||||
    const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
 | 
			
		||||
}
 | 
			
		||||
@@ -26,18 +26,16 @@ axios.interceptors.request.use(
 | 
			
		||||
    })
 | 
			
		||||
axios.interceptors.response.use(
 | 
			
		||||
    response => {
 | 
			
		||||
        let data = response.data;
 | 
			
		||||
        if (data.code === 0) {
 | 
			
		||||
            return response
 | 
			
		||||
        } else if (data.code === 400) {
 | 
			
		||||
            if (response.request.responseURL.indexOf("/api/admin") !== -1) {
 | 
			
		||||
        return response
 | 
			
		||||
    }, error => {
 | 
			
		||||
        if (error.response.status === 401 || error.response.status === 400) {
 | 
			
		||||
            if (error.response.request.responseURL.indexOf("/api/admin") !== -1) {
 | 
			
		||||
                removeAdminToken()
 | 
			
		||||
            } else {
 | 
			
		||||
                removeUserToken()
 | 
			
		||||
            }
 | 
			
		||||
            return Promise.reject(error.response.data)
 | 
			
		||||
        }
 | 
			
		||||
            return Promise.reject(response.data)
 | 
			
		||||
    }, error => {
 | 
			
		||||
        return Promise.reject(error)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -188,58 +188,13 @@ export function processContent(content) {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lines = content.split("\n")
 | 
			
		||||
    if (lines.length <= 1) {
 | 
			
		||||
        return content
 | 
			
		||||
    }
 | 
			
		||||
    const texts = []
 | 
			
		||||
    // 定义匹配数学公式的正则表达式
 | 
			
		||||
    const formulaRegex = /^\s*[a-z|A-Z]+[^=]+\s*=\s*[^=]+$/;
 | 
			
		||||
    let hasCode = false
 | 
			
		||||
    for (let i = 0; i < lines.length; i++) {
 | 
			
		||||
        // 处理引用块换行
 | 
			
		||||
        if (lines[i].startsWith(">")) {
 | 
			
		||||
            texts.push(lines[i])
 | 
			
		||||
            texts.push("\n")
 | 
			
		||||
            continue
 | 
			
		||||
        }
 | 
			
		||||
        // 如果包含代码块则跳过公式检测
 | 
			
		||||
        if (lines[i].indexOf("```") !== -1) {
 | 
			
		||||
            texts.push(lines[i])
 | 
			
		||||
            hasCode = true
 | 
			
		||||
            continue
 | 
			
		||||
        }
 | 
			
		||||
        // 识别并处理数学公式,需要排除那些已经被识别出来的公式
 | 
			
		||||
        if (i > 0 && formulaRegex.test(lines[i]) && lines[i - 1].indexOf("$$") === -1 && !hasCode) {
 | 
			
		||||
            texts.push("$$")
 | 
			
		||||
            texts.push(lines[i])
 | 
			
		||||
            texts.push("$$")
 | 
			
		||||
            continue
 | 
			
		||||
        }
 | 
			
		||||
        texts.push(lines[i])
 | 
			
		||||
    }
 | 
			
		||||
    return texts.join("\n")
 | 
			
		||||
    return content
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function processPrompt(prompt) {
 | 
			
		||||
    prompt = prompt.replace(/&/g, "&")
 | 
			
		||||
    return prompt.replace(/&/g, "&")
 | 
			
		||||
        .replace(/</g, "<")
 | 
			
		||||
        .replace(/>/g, ">");
 | 
			
		||||
 | 
			
		||||
    const linkRegex = /(https?:\/\/\S+)/g;
 | 
			
		||||
    const links = prompt.match(linkRegex);
 | 
			
		||||
    if (links) {
 | 
			
		||||
        for (let link of links) {
 | 
			
		||||
            if (isImage(link)) {
 | 
			
		||||
                const index = prompt.indexOf(link)
 | 
			
		||||
                if (prompt.substring(index - 1, 2) !== "]") {
 | 
			
		||||
                    prompt = prompt.replace(link, "\n\n")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return prompt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 判断是否为微信浏览器
 | 
			
		||||
@@ -258,3 +213,4 @@ export function showLoginDialog(router) {
 | 
			
		||||
        // on cancel
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,22 +6,14 @@
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div v-for="item in chatData" :key="item.id">
 | 
			
		||||
        <chat-prompt
 | 
			
		||||
            v-if="item.type==='prompt'"
 | 
			
		||||
            :icon="item.icon"
 | 
			
		||||
            :created-at="dateFormat(item['created_at'])"
 | 
			
		||||
            :tokens="item['tokens']"
 | 
			
		||||
            :model="item['model']"
 | 
			
		||||
            :content="item.content"/>
 | 
			
		||||
        <chat-reply v-else-if="item.type==='reply'"
 | 
			
		||||
                    :data="item" :read-only="true"/>
 | 
			
		||||
        <chat-prompt v-if="item.type==='prompt'" :data="item" list-style="list"/>
 | 
			
		||||
        <chat-reply v-else-if="item.type==='reply'" :data="item" :read-only="true" list-style="list"/>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div><!-- end chat box -->
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
 | 
			
		||||
import {dateFormat} from "@/utils/libs";
 | 
			
		||||
import ChatReply from "@/components/ChatReply.vue";
 | 
			
		||||
import ChatPrompt from "@/components/ChatPrompt.vue";
 | 
			
		||||
import {nextTick, onMounted, ref} from "vue";
 | 
			
		||||
@@ -98,7 +90,7 @@ onMounted(() => {
 | 
			
		||||
  padding 0 20px
 | 
			
		||||
 | 
			
		||||
  .chat-box {
 | 
			
		||||
    width 800px;
 | 
			
		||||
    width 100%;
 | 
			
		||||
    // 变量定义
 | 
			
		||||
    --content-font-size: 16px;
 | 
			
		||||
    --content-color: #c1c1c1;
 | 
			
		||||
@@ -110,57 +102,13 @@ onMounted(() => {
 | 
			
		||||
      text-align center
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    .chat-line-prompt {
 | 
			
		||||
    .chat-line {
 | 
			
		||||
      font-size: 14px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: flex-start;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
 | 
			
		||||
      .chat-line-inner {
 | 
			
		||||
        .chat-icon {
 | 
			
		||||
          margin-right: 0
 | 
			
		||||
        }
 | 
			
		||||
        .content {
 | 
			
		||||
          padding-top: 0
 | 
			
		||||
          font-size 16px;
 | 
			
		||||
 | 
			
		||||
          p:first-child {
 | 
			
		||||
            margin-top 0
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .chat-line-reply {
 | 
			
		||||
      padding-top: 1.5rem;
 | 
			
		||||
 | 
			
		||||
      .chat-line-inner {
 | 
			
		||||
        display flex
 | 
			
		||||
 | 
			
		||||
        .bar-item {
 | 
			
		||||
          background-color: #f7f7f8;
 | 
			
		||||
          color: #888;
 | 
			
		||||
          padding: 3px 5px;
 | 
			
		||||
          margin-right: 10px;
 | 
			
		||||
          border-radius: 5px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .chat-icon {
 | 
			
		||||
          margin-right: 20px
 | 
			
		||||
 | 
			
		||||
          img {
 | 
			
		||||
            width 30px
 | 
			
		||||
            height 30px
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            padding: 1px
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .chat-item {
 | 
			
		||||
          img {
 | 
			
		||||
            max-width 90%
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        max-width 800px
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="common-layout">
 | 
			
		||||
  <div class="chat-page">
 | 
			
		||||
    <el-container>
 | 
			
		||||
      <el-aside>
 | 
			
		||||
        <div class="chat-list">
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
          <div class="content" :style="{height: leftBoxHeight+'px'}">
 | 
			
		||||
            <el-row v-for="chat in chatList" :key="chat.chat_id">
 | 
			
		||||
              <div :class="chat.chat_id === activeChat.chat_id?'chat-list-item active':'chat-list-item'"
 | 
			
		||||
                   @click="changeChat(chat)">
 | 
			
		||||
                   @click="loadChat(chat)">
 | 
			
		||||
                <el-image :src="chat.icon" class="avatar"/>
 | 
			
		||||
                <span class="chat-title-input" v-if="chat.edit">
 | 
			
		||||
                  <el-input v-model="tmpChatTitle" size="small" @keydown="titleKeydown($event, chat)"
 | 
			
		||||
@@ -99,6 +99,12 @@
 | 
			
		||||
                </el-tag>
 | 
			
		||||
              </el-option>
 | 
			
		||||
            </el-select>
 | 
			
		||||
 | 
			
		||||
            <span class="setting" @click="showChatSetting = true">
 | 
			
		||||
                    <el-tooltip class="box-item" effect="dark" content="对话设置">
 | 
			
		||||
                      <i class="iconfont icon-config"></i>
 | 
			
		||||
                    </el-tooltip>
 | 
			
		||||
                  </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div>
 | 
			
		||||
@@ -109,13 +115,8 @@
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-for="item in chatData" :key="item.id" v-else>
 | 
			
		||||
                  <chat-prompt
 | 
			
		||||
                      v-if="item.type==='prompt'"
 | 
			
		||||
                      :icon="item.icon"
 | 
			
		||||
                      :created-at="dateFormat(item['created_at'])"
 | 
			
		||||
                      :tokens="item['tokens']"
 | 
			
		||||
                      :model="getModelValue(modelID)"
 | 
			
		||||
                      :content="item.content"/>
 | 
			
		||||
                  <chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false"/>
 | 
			
		||||
                      v-if="item.type==='prompt'" :data="item" :list-style="listStyle"/>
 | 
			
		||||
                  <chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle"/>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div><!-- end chat box -->
 | 
			
		||||
 | 
			
		||||
@@ -129,14 +130,18 @@
 | 
			
		||||
 | 
			
		||||
                  <span class="tool-item" v-if="isLogin">
 | 
			
		||||
                    <el-tooltip class="box-item" effect="dark" content="上传附件">
 | 
			
		||||
                      <file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertURL"/>
 | 
			
		||||
                      <file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertFile"/>
 | 
			
		||||
                    </el-tooltip>
 | 
			
		||||
                  </span>
 | 
			
		||||
 | 
			
		||||
                  <div class="input-body">
 | 
			
		||||
                    <div ref="textHeightRef" class="hide-div">{{prompt}}</div>
 | 
			
		||||
                    <div class="input-border">
 | 
			
		||||
                      <textarea
 | 
			
		||||
                      <div class="input-inner">
 | 
			
		||||
                        <div class="file-list" v-if="files.length > 0">
 | 
			
		||||
                          <file-list :files="files" @remove-file="removeFile" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <textarea
 | 
			
		||||
                            ref="inputRef"
 | 
			
		||||
                            class="prompt-input"
 | 
			
		||||
                            :rows="row"
 | 
			
		||||
@@ -146,6 +151,8 @@
 | 
			
		||||
                            placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
 | 
			
		||||
                            autofocus>
 | 
			
		||||
                      </textarea>
 | 
			
		||||
                      </div>
 | 
			
		||||
 | 
			
		||||
                      <span class="send-btn">
 | 
			
		||||
                        <el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
 | 
			
		||||
                          <el-icon>
 | 
			
		||||
@@ -181,21 +188,21 @@
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
 | 
			
		||||
    <ChatSetting :show="showChatSetting" @hide="showChatSetting = false"/>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
import {nextTick, onMounted, onUnmounted, ref} from 'vue'
 | 
			
		||||
import {nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
 | 
			
		||||
import ChatPrompt from "@/components/ChatPrompt.vue";
 | 
			
		||||
import ChatReply from "@/components/ChatReply.vue";
 | 
			
		||||
import {Delete, Edit, More, Plus, Promotion, Search, Share, VideoPause} from '@element-plus/icons-vue'
 | 
			
		||||
import 'highlight.js/styles/a11y-dark.css'
 | 
			
		||||
import {
 | 
			
		||||
  dateFormat,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  processContent,
 | 
			
		||||
  processPrompt,
 | 
			
		||||
  randString,
 | 
			
		||||
  removeArrayItem,
 | 
			
		||||
  UUID
 | 
			
		||||
@@ -210,6 +217,8 @@ import {checkSession} from "@/action/session";
 | 
			
		||||
import Welcome from "@/components/Welcome.vue";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import FileSelect from "@/components/FileSelect.vue";
 | 
			
		||||
import FileList from "@/components/FileList.vue";
 | 
			
		||||
import ChatSetting from "@/components/ChatSetting.vue";
 | 
			
		||||
 | 
			
		||||
const title = ref('ChatGPT-智能助手');
 | 
			
		||||
const models = ref([])
 | 
			
		||||
@@ -236,6 +245,12 @@ const notice = ref("")
 | 
			
		||||
const noticeKey = ref("SYSTEM_NOTICE")
 | 
			
		||||
const store = useSharedStore();
 | 
			
		||||
const row = ref(1)
 | 
			
		||||
const showChatSetting = ref(false)
 | 
			
		||||
const listStyle = ref(store.chatListStyle)
 | 
			
		||||
watch(() => store.chatListStyle, (newValue) => {
 | 
			
		||||
  listStyle.value = newValue
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if (isMobile()) {
 | 
			
		||||
  router.replace("/mobile/chat")
 | 
			
		||||
@@ -417,12 +432,8 @@ const newChat = () => {
 | 
			
		||||
  connect(null, roleId.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 切换会话
 | 
			
		||||
const changeChat = (chat) => {
 | 
			
		||||
  localStorage.setItem("chat_id", chat.chat_id)
 | 
			
		||||
  loadChat(chat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 切换会话
 | 
			
		||||
const loadChat = function (chat) {
 | 
			
		||||
  if (!isLogin.value) {
 | 
			
		||||
    store.setShowLoginDialog(true)
 | 
			
		||||
@@ -546,6 +557,7 @@ const socket = ref(null);
 | 
			
		||||
const activelyClose = ref(false); // 主动关闭
 | 
			
		||||
const canSend = ref(true);
 | 
			
		||||
const heartbeatHandle = ref(null)
 | 
			
		||||
const sessionId = ref("")
 | 
			
		||||
const connect = function (chat_id, role_id) {
 | 
			
		||||
  let isNewChat = false;
 | 
			
		||||
  if (!chat_id) {
 | 
			
		||||
@@ -560,7 +572,7 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
 | 
			
		||||
  const _role = getRoleById(role_id);
 | 
			
		||||
  // 初始化 WebSocket 对象
 | 
			
		||||
  const _sessionId = getSessionId();
 | 
			
		||||
  sessionId.value = getSessionId();
 | 
			
		||||
  let host = process.env.VUE_APP_WS_HOST
 | 
			
		||||
  if (host === '') {
 | 
			
		||||
    if (location.protocol === 'https:') {
 | 
			
		||||
@@ -582,7 +594,7 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
      heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
 | 
			
		||||
  const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
 | 
			
		||||
  _socket.addEventListener('open', () => {
 | 
			
		||||
    chatData.value = []; // 初始化聊天数据
 | 
			
		||||
    enableInput()
 | 
			
		||||
@@ -615,10 +627,12 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
        reader.onload = () => {
 | 
			
		||||
          const data = JSON.parse(String(reader.result));
 | 
			
		||||
          if (data.type === 'start') {
 | 
			
		||||
            const prePrompt = chatData.value[chatData.value.length-1].content
 | 
			
		||||
            chatData.value.push({
 | 
			
		||||
              type: "reply",
 | 
			
		||||
              id: randString(32),
 | 
			
		||||
              icon: _role['icon'],
 | 
			
		||||
              prompt:prePrompt,
 | 
			
		||||
              content: ""
 | 
			
		||||
            });
 | 
			
		||||
          } else if (data.type === 'end') { // 消息接收完毕
 | 
			
		||||
@@ -743,12 +757,18 @@ const sendMessage = function () {
 | 
			
		||||
  if (prompt.value.trim().length === 0 || canSend.value === false) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // 如果携带了文件,则串上文件地址
 | 
			
		||||
  let content = prompt.value
 | 
			
		||||
  if (files.value.length > 0) {
 | 
			
		||||
    content += files.value.map(file => file.url).join(" ")
 | 
			
		||||
  }
 | 
			
		||||
  // 追加消息
 | 
			
		||||
  chatData.value.push({
 | 
			
		||||
    type: "prompt",
 | 
			
		||||
    id: randString(32),
 | 
			
		||||
    icon: loginUser.value.avatar,
 | 
			
		||||
    content: md.render(processPrompt(prompt.value)),
 | 
			
		||||
    content: content,
 | 
			
		||||
    model: getModelValue(modelID.value),
 | 
			
		||||
    created_at: new Date().getTime() / 1000,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -758,9 +778,11 @@ const sendMessage = function () {
 | 
			
		||||
 | 
			
		||||
  showHello.value = false
 | 
			
		||||
  disableInput(false)
 | 
			
		||||
  socket.value.send(JSON.stringify({type: "chat", content: prompt.value}));
 | 
			
		||||
  tmpChatTitle.value = prompt.value
 | 
			
		||||
  prompt.value = '';
 | 
			
		||||
  socket.value.send(JSON.stringify({type: "chat", content: content}));
 | 
			
		||||
  tmpChatTitle.value = content
 | 
			
		||||
  prompt.value = ''
 | 
			
		||||
  files.value = []
 | 
			
		||||
  row.value = 1
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -810,9 +832,11 @@ const loadChatHistory = function (chatId) {
 | 
			
		||||
    showHello.value = false
 | 
			
		||||
    for (let i = 0; i < data.length; i++) {
 | 
			
		||||
      data[i].orgContent = data[i].content;
 | 
			
		||||
      data[i].content = md.render(processContent(data[i].content))
 | 
			
		||||
      if (i > 0 && data[i].type === 'reply') {
 | 
			
		||||
        data[i].prompt = data[i - 1].orgContent
 | 
			
		||||
      if (data[i].type === 'reply') {
 | 
			
		||||
        data[i].content = md.render(processContent(data[i].content))
 | 
			
		||||
        if (i > 0) {
 | 
			
		||||
          data[i].prompt = data[i - 1].orgContent
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      chatData.value.push(data[i]);
 | 
			
		||||
    }
 | 
			
		||||
@@ -829,7 +853,7 @@ const loadChatHistory = function (chatId) {
 | 
			
		||||
 | 
			
		||||
const stopGenerate = function () {
 | 
			
		||||
  showStopGenerate.value = false;
 | 
			
		||||
  httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
 | 
			
		||||
  httpGet("/api/chat/stop?session_id=" + sessionId.value).then(() => {
 | 
			
		||||
    enableInput()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
@@ -843,7 +867,7 @@ const reGenerate = function (prompt) {
 | 
			
		||||
    type: "prompt",
 | 
			
		||||
    id: randString(32),
 | 
			
		||||
    icon: loginUser.value.avatar,
 | 
			
		||||
    content: md.render(text)
 | 
			
		||||
    content: text
 | 
			
		||||
  });
 | 
			
		||||
  socket.value.send(JSON.stringify({type: "chat", content: prompt}));
 | 
			
		||||
}
 | 
			
		||||
@@ -893,9 +917,13 @@ const notShow = () => {
 | 
			
		||||
  showNotice.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 插入文件路径
 | 
			
		||||
const insertURL = (url) => {
 | 
			
		||||
  prompt.value += " " + url + " "
 | 
			
		||||
const files = ref([])
 | 
			
		||||
// 插入文件
 | 
			
		||||
const insertFile = (file) => {
 | 
			
		||||
  files.value.push(file)
 | 
			
		||||
}
 | 
			
		||||
const removeFile = (file) => {
 | 
			
		||||
  files.value = removeArrayItem(files.value, file, (v1,v2) => v1.url===v2.url)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -86,43 +86,7 @@
 | 
			
		||||
          <div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
 | 
			
		||||
            <div class="job-list-box">
 | 
			
		||||
              <h2>任务列表</h2>
 | 
			
		||||
              <div class="running-job-list">
 | 
			
		||||
                <div class="running-job-box" v-if="runningJobs.length > 0">
 | 
			
		||||
                  <div class="job-item" v-for="item in runningJobs" :key="item.id">
 | 
			
		||||
                    <div v-if="item.progress > 0" class="job-item-inner">
 | 
			
		||||
                      <el-image :src="item['img_url']" fit="cover" loading="lazy">
 | 
			
		||||
                        <template #placeholder>
 | 
			
		||||
                          <div class="image-slot">
 | 
			
		||||
                            正在加载图片
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
 | 
			
		||||
                        <template #error>
 | 
			
		||||
                          <div class="image-slot">
 | 
			
		||||
                            <el-icon>
 | 
			
		||||
                              <Picture/>
 | 
			
		||||
                            </el-icon>
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
                      </el-image>
 | 
			
		||||
 | 
			
		||||
                      <div class="progress">
 | 
			
		||||
                        <el-progress type="circle" :percentage="item.progress" :width="100"
 | 
			
		||||
                                     color="#47fff1"/>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <el-image fit="cover" v-else>
 | 
			
		||||
                      <template #error>
 | 
			
		||||
                        <div class="image-slot">
 | 
			
		||||
                          <i class="iconfont icon-quick-start"></i>
 | 
			
		||||
                          <span>任务正在排队中</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </template>
 | 
			
		||||
                    </el-image>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <el-empty :image-size="100" v-else/>
 | 
			
		||||
              </div>
 | 
			
		||||
              <task-list :list="runningJobs" />
 | 
			
		||||
 | 
			
		||||
              <h2>创作记录</h2>
 | 
			
		||||
              <div class="finish-job-list">
 | 
			
		||||
@@ -228,6 +192,7 @@ import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
 | 
			
		||||
import Clipboard from "clipboard";
 | 
			
		||||
import {checkSession} from "@/action/session";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import TaskList from "@/components/TaskList.vue";
 | 
			
		||||
 | 
			
		||||
const listBoxHeight = ref(0)
 | 
			
		||||
// const paramBoxHeight = ref(0)
 | 
			
		||||
@@ -369,7 +334,7 @@ const fetchRunningJobs = () => {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  // 获取运行中的任务
 | 
			
		||||
  httpGet(`/api/dall/jobs?status=0`).then(res => {
 | 
			
		||||
  httpGet(`/api/dall/jobs?finish=false`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
@@ -402,7 +367,7 @@ const fetchFinishJobs = () => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  page.value = page.value + 1
 | 
			
		||||
 | 
			
		||||
  httpGet(`/api/dall/jobs?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
  httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    if (res.data.length < pageSize.value) {
 | 
			
		||||
      isOver.value = true
 | 
			
		||||
    }
 | 
			
		||||
@@ -454,7 +419,7 @@ const removeImage = (event, item) => {
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
      }
 | 
			
		||||
  ).then(() => {
 | 
			
		||||
    httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
    httpGet("/api/dall/remove", {id: item.id, user_id: item.user}).then(() => {
 | 
			
		||||
      ElMessage.success("任务删除成功")
 | 
			
		||||
      page.value = 0
 | 
			
		||||
      isOver.value = false
 | 
			
		||||
@@ -477,7 +442,7 @@ const publishImage = (event, item, action) => {
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
  httpGet("/api/dall/publish", {id: item.id, action: action,user_id:item.user_id}).then(() => {
 | 
			
		||||
    ElMessage.success(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
    page.value = 0
 | 
			
		||||
 
 | 
			
		||||
@@ -49,15 +49,15 @@
 | 
			
		||||
              <div  v-if="!licenseConfig.de_copy">
 | 
			
		||||
                <el-dropdown-item>
 | 
			
		||||
                  <i class="iconfont icon-book"></i>
 | 
			
		||||
                  <a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
 | 
			
		||||
                  <a :href="docsURL" target="_blank">
 | 
			
		||||
                    用户手册
 | 
			
		||||
                  </a>
 | 
			
		||||
                </el-dropdown-item>
 | 
			
		||||
 | 
			
		||||
                <el-dropdown-item>
 | 
			
		||||
                  <i class="iconfont icon-github"></i>
 | 
			
		||||
                  <a href="https://ai.r9it.com/docs/" target="_blank">
 | 
			
		||||
                    Geek-AI {{ version }}
 | 
			
		||||
                  <a :href="gitURL" target="_blank">
 | 
			
		||||
                    GeekAI {{ version }}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </el-dropdown-item>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -142,7 +142,7 @@ import {checkSession} from "@/action/session";
 | 
			
		||||
import {removeUserToken} from "@/store/session";
 | 
			
		||||
import LoginDialog from "@/components/LoginDialog.vue";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import ConfigDialog from "@/components/ConfigDialog.vue";
 | 
			
		||||
import ConfigDialog from "@/components/UserInfoDialog.vue";
 | 
			
		||||
import {showMessageError} from "@/utils/dialog";
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
@@ -157,6 +157,8 @@ const version = ref(process.env.VUE_APP_VERSION)
 | 
			
		||||
const routerViewKey = ref(0)
 | 
			
		||||
const showConfigDialog = ref(false)
 | 
			
		||||
const licenseConfig = ref({})
 | 
			
		||||
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)
 | 
			
		||||
@@ -215,10 +217,11 @@ const init = () => {
 | 
			
		||||
const logout = function () {
 | 
			
		||||
  httpGet('/api/user/logout').then(() => {
 | 
			
		||||
    removeUserToken()
 | 
			
		||||
    store.setShowLoginDialog(true)
 | 
			
		||||
    loginUser.value = {}
 | 
			
		||||
    // 刷新组件
 | 
			
		||||
    routerViewKey.value += 1
 | 
			
		||||
    router.push("/login")
 | 
			
		||||
    // store.setShowLoginDialog(true)
 | 
			
		||||
    // loginUser.value = {}
 | 
			
		||||
    // // 刷新组件
 | 
			
		||||
    // routerViewKey.value += 1
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    ElMessage.error('注销失败!');
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -163,7 +163,7 @@
 | 
			
		||||
          </el-form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="task-list-box" @scrollend="handleScrollEnd">
 | 
			
		||||
      <div class="task-list-box">
 | 
			
		||||
        <div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
 | 
			
		||||
          <div class="extra-params">
 | 
			
		||||
            <el-form>
 | 
			
		||||
@@ -450,43 +450,7 @@
 | 
			
		||||
 | 
			
		||||
          <div class="job-list-box">
 | 
			
		||||
            <h2>任务列表</h2>
 | 
			
		||||
            <div class="running-job-list">
 | 
			
		||||
              <div class="running-job-box" v-if="runningJobs.length > 0">
 | 
			
		||||
                <div class="job-item" v-for="item in runningJobs">
 | 
			
		||||
                  <div v-if="item.progress > 0" class="job-item-inner">
 | 
			
		||||
                    <el-image :src="item['img_url']" fit="cover" loading="lazy">
 | 
			
		||||
                      <template #placeholder>
 | 
			
		||||
                        <div class="image-slot">
 | 
			
		||||
                          正在加载图片
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </template>
 | 
			
		||||
 | 
			
		||||
                      <template #error>
 | 
			
		||||
                        <div class="image-slot">
 | 
			
		||||
                          <el-icon>
 | 
			
		||||
                            <Picture/>
 | 
			
		||||
                          </el-icon>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </template>
 | 
			
		||||
                    </el-image>
 | 
			
		||||
 | 
			
		||||
                    <div class="progress">
 | 
			
		||||
                      <el-progress type="circle" :percentage="item.progress" :width="100"
 | 
			
		||||
                                   color="#47fff1"/>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <el-image fit="cover" v-else>
 | 
			
		||||
                    <template #error>
 | 
			
		||||
                      <div class="image-slot">
 | 
			
		||||
                        <i class="iconfont icon-quick-start"></i>
 | 
			
		||||
                        <span>任务正在排队中</span>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </template>
 | 
			
		||||
                  </el-image>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <el-empty :image-size="100" v-else/>
 | 
			
		||||
            </div>
 | 
			
		||||
            <task-list :list="runningJobs" />
 | 
			
		||||
 | 
			
		||||
            <h2>创作记录</h2>
 | 
			
		||||
            <div class="finish-job-list">
 | 
			
		||||
@@ -617,6 +581,7 @@ import {useRouter} from "vue-router";
 | 
			
		||||
import {getSessionId} from "@/store/session";
 | 
			
		||||
import {copyObj, removeArrayItem} from "@/utils/libs";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import TaskList from "@/components/TaskList.vue";
 | 
			
		||||
 | 
			
		||||
const listBoxHeight = ref(0)
 | 
			
		||||
const paramBoxHeight = ref(0)
 | 
			
		||||
@@ -817,7 +782,7 @@ const fetchRunningJobs = () => {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  httpGet(`/api/mj/jobs?status=0`).then(res => {
 | 
			
		||||
  httpGet(`/api/mj/jobs?finish=false`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
@@ -855,7 +820,7 @@ const fetchFinishJobs = () => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  page.value = page.value + 1
 | 
			
		||||
  // 获取已完成的任务
 | 
			
		||||
  httpGet(`/api/mj/jobs?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
  httpGet(`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
      if (jobs[i]['img_url'] !== "") {
 | 
			
		||||
@@ -996,7 +961,7 @@ const removeImage = (item) => {
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
      }
 | 
			
		||||
  ).then(() => {
 | 
			
		||||
    httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
    httpGet("/api/mj/remove", {id: item.id, user_id: item.user_id}).then(() => {
 | 
			
		||||
      ElMessage.success("任务删除成功")
 | 
			
		||||
      page.value = 0
 | 
			
		||||
      isOver.value = false
 | 
			
		||||
@@ -1014,7 +979,7 @@ const publishImage = (item, action) => {
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
  httpGet("/api/mj/publish", {id: item.id, action: action,user_id: item.user_id}).then(() => {
 | 
			
		||||
    ElMessage.success(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
    page.value = 0
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="param-line" style="padding-top: 10px">
 | 
			
		||||
                <el-form-item label="采样调度器">
 | 
			
		||||
                <el-form-item label="采样调度">
 | 
			
		||||
                  <template #default>
 | 
			
		||||
                    <div class="form-item-inner">
 | 
			
		||||
                      <el-select v-model="params.scheduler" style="width:176px">
 | 
			
		||||
@@ -296,39 +296,8 @@
 | 
			
		||||
          <div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
 | 
			
		||||
            <div class="job-list-box">
 | 
			
		||||
              <h2>任务列表</h2>
 | 
			
		||||
              <div class="running-job-list">
 | 
			
		||||
                <div class="running-job-box" v-if="runningJobs.length > 0">
 | 
			
		||||
                  <div class="job-item" v-for="item in runningJobs">
 | 
			
		||||
                    <div v-if="item.progress > 0" class="job-item-inner">
 | 
			
		||||
                      <el-image :src="item['img_url']" fit="cover" loading="lazy">
 | 
			
		||||
                        <template #placeholder>
 | 
			
		||||
                          <div class="image-slot">
 | 
			
		||||
                            正在加载图片
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
 | 
			
		||||
                        <template #error>
 | 
			
		||||
                          <div class="image-slot"></div>
 | 
			
		||||
                        </template>
 | 
			
		||||
                      </el-image>
 | 
			
		||||
 | 
			
		||||
                      <div class="progress">
 | 
			
		||||
                        <el-progress type="circle" :percentage="item.progress" :width="100"
 | 
			
		||||
                                     color="#47fff1"/>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <el-image fit="cover" v-else>
 | 
			
		||||
                      <template #error>
 | 
			
		||||
                        <div class="image-slot">
 | 
			
		||||
                          <i class="iconfont icon-quick-start"></i>
 | 
			
		||||
                          <span>任务正在排队中</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </template>
 | 
			
		||||
                    </el-image>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <el-empty :image-size="100" v-else/>
 | 
			
		||||
              </div>
 | 
			
		||||
              <task-list :list="runningJobs" />
 | 
			
		||||
              
 | 
			
		||||
              <h2>创作记录</h2>
 | 
			
		||||
              <div class="finish-job-list">
 | 
			
		||||
                <div v-if="finishedJobs.length > 0">
 | 
			
		||||
@@ -506,6 +475,7 @@ import {checkSession} from "@/action/session";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {getSessionId} from "@/store/session";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import TaskList from "@/components/TaskList.vue";
 | 
			
		||||
 | 
			
		||||
const listBoxHeight = ref(0)
 | 
			
		||||
// const paramBoxHeight = ref(0)
 | 
			
		||||
@@ -661,7 +631,7 @@ const fetchRunningJobs = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 获取运行中的任务
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=0`).then(res => {
 | 
			
		||||
  httpGet(`/api/sd/jobs?finish=0`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
@@ -694,7 +664,7 @@ const fetchFinishJobs = () => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  page.value = page.value + 1
 | 
			
		||||
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
  httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    if (res.data.length < pageSize.value) {
 | 
			
		||||
      isOver.value = true
 | 
			
		||||
    }
 | 
			
		||||
@@ -761,7 +731,7 @@ const removeImage = (event, item) => {
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
      }
 | 
			
		||||
  ).then(() => {
 | 
			
		||||
    httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
    httpGet("/api/sd/remove", {id: item.id, user_id: item.user}).then(() => {
 | 
			
		||||
      ElMessage.success("任务删除成功")
 | 
			
		||||
      page.value = 0
 | 
			
		||||
      isOver.value = false
 | 
			
		||||
@@ -780,7 +750,7 @@ const publishImage = (event, item, action) => {
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
  httpGet("/api/sd/publish", {id: item.id, action: action, user_id: item.user}).then(() => {
 | 
			
		||||
    ElMessage.success(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
    page.value = 0
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="index-page" :style="{height: winHeight+'px'}">
 | 
			
		||||
    <div :class="bgClass"></div>
 | 
			
		||||
    <div class="index-bg" :style="{backgroundImage: 'url('+bgImgUrl+')'}"></div>
 | 
			
		||||
    <div class="menu-box">
 | 
			
		||||
      <el-menu
 | 
			
		||||
          mode="horizontal"
 | 
			
		||||
@@ -12,17 +12,17 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="menu-item">
 | 
			
		||||
          <span v-if="!licenseConfig.de_copy">
 | 
			
		||||
            <a href="https://ai.r9it.com/docs/install/" target="_blank">
 | 
			
		||||
            <a :href="docsURL" target="_blank">
 | 
			
		||||
            <el-button type="primary" round>
 | 
			
		||||
              <i class="iconfont icon-book"></i>
 | 
			
		||||
              <span>部署文档</span>
 | 
			
		||||
              <span>文档</span>
 | 
			
		||||
            </el-button>
 | 
			
		||||
          </a>
 | 
			
		||||
 | 
			
		||||
          <a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
 | 
			
		||||
          <a :href="gitURL" target="_blank">
 | 
			
		||||
            <el-button type="success" round>
 | 
			
		||||
              <i class="iconfont icon-github"></i>
 | 
			
		||||
              <span>项目源码</span>
 | 
			
		||||
              <span>源码</span>
 | 
			
		||||
            </el-button>
 | 
			
		||||
          </a>
 | 
			
		||||
          </span>
 | 
			
		||||
@@ -65,7 +65,6 @@
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
 | 
			
		||||
// import * as THREE from 'three';
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import FooterBar from "@/components/FooterBar.vue";
 | 
			
		||||
@@ -84,17 +83,20 @@ const title = ref("Geek-AI 创作系统")
 | 
			
		||||
const logo = ref("/images/logo.png")
 | 
			
		||||
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
 | 
			
		||||
const licenseConfig = ref({})
 | 
			
		||||
// const size = Math.max(window.innerWidth * 0.5, window.innerHeight * 0.8)
 | 
			
		||||
const winHeight = window.innerHeight - 150
 | 
			
		||||
const bgClass = ref('fixed-bg')
 | 
			
		||||
const bgImgUrl = ref('')
 | 
			
		||||
const isLogin = ref(false)
 | 
			
		||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
 | 
			
		||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  httpGet("/api/config/get?key=system").then(res => {
 | 
			
		||||
    title.value = res.data.title
 | 
			
		||||
    logo.value = res.data.logo
 | 
			
		||||
    if (res.data.rand_bg) {
 | 
			
		||||
      bgClass.value = "rand-bg"
 | 
			
		||||
    if (res.data.index_bg_url) {
 | 
			
		||||
      bgImgUrl.value = res.data.index_bg_url
 | 
			
		||||
    } else {
 | 
			
		||||
      bgImgUrl.value = "/images/index-bg.jpg"
 | 
			
		||||
    }
 | 
			
		||||
    if (res.data.slogan) {
 | 
			
		||||
      slogan.value = res.data.slogan
 | 
			
		||||
@@ -126,24 +128,12 @@ onMounted(() => {
 | 
			
		||||
  align-items baseline
 | 
			
		||||
  padding-top 150px
 | 
			
		||||
 | 
			
		||||
  .fixed-bg {
 | 
			
		||||
  .index-bg {
 | 
			
		||||
    position absolute
 | 
			
		||||
    top 0
 | 
			
		||||
    left 0
 | 
			
		||||
    width 100vw
 | 
			
		||||
    height 100vh
 | 
			
		||||
    background-image url("~@/assets/img/ai-bg.jpg")
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
    background-position: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .rand-bg {
 | 
			
		||||
    position absolute
 | 
			
		||||
    top 0
 | 
			
		||||
    left 0
 | 
			
		||||
    width 100vw
 | 
			
		||||
    height 100vh
 | 
			
		||||
    background-image url("https://api.dujin.org/bing/1920.php")
 | 
			
		||||
    filter: blur(8px);
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
    background-position: center;
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
            <el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
 | 
			
		||||
          </el-row>
 | 
			
		||||
 | 
			
		||||
          <el-row class="opt" :gutter="20">
 | 
			
		||||
          <el-row class="opt" :gutter="24">
 | 
			
		||||
            <el-col :span="8"><el-link type="primary" @click="router.push('/register')">注册</el-link></el-col>
 | 
			
		||||
            <el-col :span="8">
 | 
			
		||||
              <el-link type="info" @click="showResetPass = true">重置密码</el-link>
 | 
			
		||||
@@ -43,6 +43,14 @@
 | 
			
		||||
              <el-link type="info" @click="router.push('/')">首页</el-link>
 | 
			
		||||
            </el-col>
 | 
			
		||||
          </el-row>
 | 
			
		||||
 | 
			
		||||
          <div v-if="wechatLoginURL !== ''">
 | 
			
		||||
            <el-divider class="divider">其他登录方式</el-divider>
 | 
			
		||||
 | 
			
		||||
            <div class="clogin">
 | 
			
		||||
              <a class="wechat-login" :href="wechatLoginURL"><i class="iconfont icon-wechat"></i></a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +65,7 @@
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
@@ -75,28 +83,38 @@ const password = ref(process.env.VUE_APP_PASS);
 | 
			
		||||
const showResetPass = ref(false)
 | 
			
		||||
const logo = ref("/images/logo.png")
 | 
			
		||||
const licenseConfig = ref({})
 | 
			
		||||
const wechatLoginURL = ref('')
 | 
			
		||||
 | 
			
		||||
// 获取系统配置
 | 
			
		||||
httpGet("/api/config/get?key=system").then(res => {
 | 
			
		||||
  logo.value = res.data.logo
 | 
			
		||||
  title.value = res.data.title
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
  showMessageError("获取系统配置失败:" + e.message)
 | 
			
		||||
})
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 获取系统配置
 | 
			
		||||
  httpGet("/api/config/get?key=system").then(res => {
 | 
			
		||||
    logo.value = res.data.logo
 | 
			
		||||
    title.value = res.data.title
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("获取系统配置失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
httpGet("/api/config/license").then(res => {
 | 
			
		||||
  licenseConfig.value = res.data
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
  showMessageError("获取 License 配置:" + e.message)
 | 
			
		||||
})
 | 
			
		||||
  httpGet("/api/config/license").then(res => {
 | 
			
		||||
    licenseConfig.value = res.data
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("获取 License 配置:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
checkSession().then(() => {
 | 
			
		||||
  if (isMobile()) {
 | 
			
		||||
    router.push('/mobile')
 | 
			
		||||
  } else {
 | 
			
		||||
    router.push('/chat')
 | 
			
		||||
  }
 | 
			
		||||
}).catch(() => {
 | 
			
		||||
  checkSession().then(() => {
 | 
			
		||||
    if (isMobile()) {
 | 
			
		||||
      router.push('/mobile')
 | 
			
		||||
    } else {
 | 
			
		||||
      router.push('/chat')
 | 
			
		||||
    }
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const returnURL = `${location.protocol}//${location.host}/login/callback`
 | 
			
		||||
  httpGet("/api/user/clogin?return_url="+returnURL).then(res => {
 | 
			
		||||
    wechatLoginURL.value = res.data.url
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    console.error(e)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleKeyup = (e) => {
 | 
			
		||||
@@ -114,7 +132,7 @@ const login = function () {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
 | 
			
		||||
    setUserToken(res.data)
 | 
			
		||||
    setUserToken(res.data.token)
 | 
			
		||||
    if (isMobile()) {
 | 
			
		||||
      router.push('/mobile')
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -129,99 +147,5 @@ const login = function () {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.bg {
 | 
			
		||||
  position fixed
 | 
			
		||||
  left 0
 | 
			
		||||
  right 0
 | 
			
		||||
  top 0
 | 
			
		||||
  bottom 0
 | 
			
		||||
  background-color #313237
 | 
			
		||||
  background-image url("~@/assets/img/login-bg.jpg")
 | 
			
		||||
  background-size cover
 | 
			
		||||
  background-position center
 | 
			
		||||
  background-repeat repeat-y
 | 
			
		||||
  //filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
  .contain {
 | 
			
		||||
    position fixed
 | 
			
		||||
    left 50%
 | 
			
		||||
    top 40%
 | 
			
		||||
    width 90%
 | 
			
		||||
    max-width 400px;
 | 
			
		||||
    transform translate(-50%, -50%)
 | 
			
		||||
    padding 20px 10px;
 | 
			
		||||
    color #ffffff
 | 
			
		||||
    border-radius 10px;
 | 
			
		||||
 | 
			
		||||
    .logo {
 | 
			
		||||
      text-align center
 | 
			
		||||
 | 
			
		||||
      .el-image {
 | 
			
		||||
        width 120px;
 | 
			
		||||
        cursor pointer
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .header {
 | 
			
		||||
      width 100%
 | 
			
		||||
      margin-bottom 24px
 | 
			
		||||
      font-size 24px
 | 
			
		||||
      color $white_v1
 | 
			
		||||
      letter-space 2px
 | 
			
		||||
      text-align center
 | 
			
		||||
      padding-top 10px
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .content {
 | 
			
		||||
      width 100%
 | 
			
		||||
      height: auto
 | 
			
		||||
      border-radius 3px
 | 
			
		||||
 | 
			
		||||
      .block {
 | 
			
		||||
        margin-bottom 16px
 | 
			
		||||
 | 
			
		||||
        .el-input__inner {
 | 
			
		||||
          border 1px solid $gray-v6 !important
 | 
			
		||||
 | 
			
		||||
          .el-icon-user, .el-icon-lock {
 | 
			
		||||
            font-size 20px
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .btn-row {
 | 
			
		||||
        padding-top 10px;
 | 
			
		||||
 | 
			
		||||
        .login-btn {
 | 
			
		||||
          width 100%
 | 
			
		||||
          font-size 16px
 | 
			
		||||
          letter-spacing 2px
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .text-line {
 | 
			
		||||
        justify-content center
 | 
			
		||||
        padding-top 10px;
 | 
			
		||||
        font-size 14px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .opt {
 | 
			
		||||
        padding 15px
 | 
			
		||||
        .el-col {
 | 
			
		||||
          text-align center
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .footer {
 | 
			
		||||
    color #ffffff;
 | 
			
		||||
 | 
			
		||||
    .container {
 | 
			
		||||
      padding 20px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@import "@/assets/css/login.styl"
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										105
									
								
								web/src/views/LoginCallback.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,105 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="login-callback"
 | 
			
		||||
       v-loading="loading"
 | 
			
		||||
       element-loading-text="正在同步登录信息..."
 | 
			
		||||
       :style="{ height: winHeight + 'px' }">
 | 
			
		||||
 | 
			
		||||
    <el-dialog
 | 
			
		||||
        v-model="show"
 | 
			
		||||
        :close-on-click-modal="false"
 | 
			
		||||
        :show-close="false"
 | 
			
		||||
        style="width: 360px;"
 | 
			
		||||
    >
 | 
			
		||||
      <el-result
 | 
			
		||||
          icon="success"
 | 
			
		||||
          title="登录成功"
 | 
			
		||||
          style="--el-result-padding:10px"
 | 
			
		||||
      >
 | 
			
		||||
          <template #sub-title>
 | 
			
		||||
            <div class="user-info">
 | 
			
		||||
              <div class="line">您的初始账户信息如下:</div>
 | 
			
		||||
              <div class="line"><span>用户名:</span>{{username}}</div>
 | 
			
		||||
              <div class="line"><span>密码:</span>{{password}}</div>
 | 
			
		||||
              <div class="line">您后期也可以通过此账号和密码登录</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template #extra>
 | 
			
		||||
            <el-button type="primary" @click="finishLogin">我知道了</el-button>
 | 
			
		||||
          </template>
 | 
			
		||||
      </el-result>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue"
 | 
			
		||||
import {useRouter} from "vue-router"
 | 
			
		||||
import {ElMessage, ElMessageBox} from "element-plus";
 | 
			
		||||
import {httpGet} from "@/utils/http";
 | 
			
		||||
import {setUserToken} from "@/store/session";
 | 
			
		||||
import {isMobile} from "@/utils/libs";
 | 
			
		||||
 | 
			
		||||
const winHeight = ref(window.innerHeight)
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const show = ref(false)
 | 
			
		||||
const username = ref('')
 | 
			
		||||
const password = ref('')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const code = router.currentRoute.value.query.code
 | 
			
		||||
if (code === "") {
 | 
			
		||||
  ElMessage.error({message: "登录失败:code 参数不能为空",duration: 2000, onClose: () => router.push("/")})
 | 
			
		||||
} else {
 | 
			
		||||
  // 发送请求获取用户信息
 | 
			
		||||
  httpGet("/api/user/clogin/callback",{login_type: "wx",code: code}).then(res => {
 | 
			
		||||
    setUserToken(res.data.token)
 | 
			
		||||
    if (res.data.username) {
 | 
			
		||||
      username.value = res.data.username
 | 
			
		||||
      password.value = res.data.password
 | 
			
		||||
      show.value = true
 | 
			
		||||
      loading.value = false
 | 
			
		||||
    } else {
 | 
			
		||||
      finishLogin()
 | 
			
		||||
    }
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessageBox.alert(e.message, {
 | 
			
		||||
      confirmButtonText: '重新登录',
 | 
			
		||||
      type:"error",
 | 
			
		||||
      title:"登录失败",
 | 
			
		||||
      callback: () => {
 | 
			
		||||
        router.push("/login")
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
const finishLogin = () => {
 | 
			
		||||
  if (isMobile()) {
 | 
			
		||||
    router.push('/mobile')
 | 
			
		||||
  } else {
 | 
			
		||||
    router.push('/chat')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.login-callback {
 | 
			
		||||
  .user-info {
 | 
			
		||||
    display flex
 | 
			
		||||
    flex-direction column
 | 
			
		||||
    padding 10px
 | 
			
		||||
    border 1px dashed #e1e1e1
 | 
			
		||||
    border-radius 10px
 | 
			
		||||
    .line {
 | 
			
		||||
      text-align left
 | 
			
		||||
      font-size 14px
 | 
			
		||||
      line-height 1.5
 | 
			
		||||
 | 
			
		||||
      span{
 | 
			
		||||
        font-weight bold
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -333,7 +333,7 @@ const wechatPay = (row) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const queryOrder = (orderNo) => {
 | 
			
		||||
  httpPost("/api/payment/query", {order_no: orderNo}).then(res => {
 | 
			
		||||
  httpGet("/api/order/query", {order_no: orderNo}).then(res => {
 | 
			
		||||
    if (res.data.status === 1) {
 | 
			
		||||
      text.value = "扫码成功,请在手机上进行支付!"
 | 
			
		||||
      queryOrder(orderNo)
 | 
			
		||||
 
 | 
			
		||||
@@ -178,7 +178,7 @@ const tableData = ref([])
 | 
			
		||||
const sortedTableData = ref([])
 | 
			
		||||
const role = ref({context: []})
 | 
			
		||||
const formRef = ref(null)
 | 
			
		||||
const optTitle = ref({})
 | 
			
		||||
const optTitle = ref("")
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
 | 
			
		||||
const rules = reactive({
 | 
			
		||||
@@ -231,7 +231,7 @@ const fetchData = () => {
 | 
			
		||||
      sortedData.forEach((id, index) => {
 | 
			
		||||
        ids.push(parseInt(id))
 | 
			
		||||
        sorts.push(index+1)
 | 
			
		||||
        items.value[index].sort_num = index + 1
 | 
			
		||||
        tableData.value[index].sort_num = index + 1
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      httpPost("/api/admin/role/sort", {ids: ids, sorts: sorts}).catch(e => {
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@
 | 
			
		||||
        v-model="showChatItemDialog"
 | 
			
		||||
        title="对话详情"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="chat-box common-layout">
 | 
			
		||||
      <div class="chat-box chat-page">
 | 
			
		||||
        <div v-for="item in messages" :key="item.id">
 | 
			
		||||
          <chat-prompt
 | 
			
		||||
              v-if="item.type==='prompt'"
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,7 @@
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="绑定API-KEY:" prop="apikey">
 | 
			
		||||
          <el-select v-model="item.key_id" placeholder="请选择 API KEY" clearable>
 | 
			
		||||
          <el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
 | 
			
		||||
            <el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
 | 
			
		||||
              {{ v.name }}
 | 
			
		||||
              <el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
 | 
			
		||||
@@ -229,7 +229,7 @@ const platforms = ref([])
 | 
			
		||||
 | 
			
		||||
// 获取 API KEY
 | 
			
		||||
const apiKeys = ref([])
 | 
			
		||||
httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
 | 
			
		||||
httpGet('/api/admin/apikey/list?type=chat').then(res => {
 | 
			
		||||
  apiKeys.value = res.data
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
  ElMessage.error("获取 API KEY 失败:" + e.message)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,32 +33,42 @@
 | 
			
		||||
                  </el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item label="开放注册" prop="enabled_register">
 | 
			
		||||
                  <el-switch v-model="system['enabled_register']"/>
 | 
			
		||||
                  <el-tooltip
 | 
			
		||||
                      effect="dark"
 | 
			
		||||
                      content="关闭注册之后只能通过管理后台添加用户"
 | 
			
		||||
                      raw-content
 | 
			
		||||
                      placement="right"
 | 
			
		||||
                  >
 | 
			
		||||
                    <el-icon>
 | 
			
		||||
                      <InfoFilled/>
 | 
			
		||||
                    </el-icon>
 | 
			
		||||
                  </el-tooltip>
 | 
			
		||||
                <el-form-item label="首页背景图" prop="logo">
 | 
			
		||||
                  <div class="tip-input">
 | 
			
		||||
                    <el-input v-model="system['index_bg_url']" placeholder="网站首页背景图片">
 | 
			
		||||
                      <template #append>
 | 
			
		||||
                        <el-upload
 | 
			
		||||
                            :auto-upload="true"
 | 
			
		||||
                            :show-file-list="false"
 | 
			
		||||
                            @click="beforeUpload('index_bg_url')"
 | 
			
		||||
                            :http-request="uploadImg"
 | 
			
		||||
                        >
 | 
			
		||||
                          <el-icon class="uploader-icon">
 | 
			
		||||
                            <UploadFilled/>
 | 
			
		||||
                          </el-icon>
 | 
			
		||||
                        </el-upload>
 | 
			
		||||
                      </template>
 | 
			
		||||
                    </el-input>
 | 
			
		||||
                    <el-button type="primary" @click="system.index_bg_url = 'https://api.dujin.org/bing/1920.php'">使用动态背景</el-button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item label="动态背景">
 | 
			
		||||
                  <el-switch v-model="system['rand_bg']"/>
 | 
			
		||||
                  <el-tooltip
 | 
			
		||||
                      effect="dark"
 | 
			
		||||
                      content="打开之后前端首页将使用随机壁纸作为背景图"
 | 
			
		||||
                      raw-content
 | 
			
		||||
                      placement="right"
 | 
			
		||||
                  >
 | 
			
		||||
                    <el-icon>
 | 
			
		||||
                      <InfoFilled/>
 | 
			
		||||
                    </el-icon>
 | 
			
		||||
                  </el-tooltip>
 | 
			
		||||
                <el-form-item label="开放注册" prop="enabled_register">
 | 
			
		||||
                  <div class="tip-input">
 | 
			
		||||
                    <el-switch v-model="system['enabled_register']"/>
 | 
			
		||||
                    <div class="info">
 | 
			
		||||
                      <el-tooltip
 | 
			
		||||
                          effect="dark"
 | 
			
		||||
                          content="关闭注册之后只能通过管理后台添加用户"
 | 
			
		||||
                          raw-content
 | 
			
		||||
                          placement="right"
 | 
			
		||||
                      >
 | 
			
		||||
                        <el-icon>
 | 
			
		||||
                          <InfoFilled/>
 | 
			
		||||
                        </el-icon>
 | 
			
		||||
                      </el-tooltip>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item label="注册方式" prop="register_ways">
 | 
			
		||||
@@ -211,17 +221,21 @@
 | 
			
		||||
              </el-tab-pane>
 | 
			
		||||
              <el-tab-pane label="众筹支付">
 | 
			
		||||
                <el-form-item label="启用众筹功能" prop="enabled_reward">
 | 
			
		||||
                  <el-switch v-model="system['enabled_reward']"/>
 | 
			
		||||
                  <el-tooltip
 | 
			
		||||
                      effect="dark"
 | 
			
		||||
                      content="如果关闭次功能将不在用户菜单显示众筹二维码"
 | 
			
		||||
                      raw-content
 | 
			
		||||
                      placement="right"
 | 
			
		||||
                  >
 | 
			
		||||
                    <el-icon>
 | 
			
		||||
                      <InfoFilled/>
 | 
			
		||||
                    </el-icon>
 | 
			
		||||
                  </el-tooltip>
 | 
			
		||||
                  <div class="tip-input">
 | 
			
		||||
                    <el-switch v-model="system['enabled_reward']"/>
 | 
			
		||||
                    <div class="info">
 | 
			
		||||
                      <el-tooltip
 | 
			
		||||
                          effect="dark"
 | 
			
		||||
                          content="如果关闭次功能将不在用户菜单显示众筹二维码"
 | 
			
		||||
                          raw-content
 | 
			
		||||
                          placement="right"
 | 
			
		||||
                      >
 | 
			
		||||
                        <el-icon>
 | 
			
		||||
                          <InfoFilled/>
 | 
			
		||||
                        </el-icon>
 | 
			
		||||
                      </el-tooltip>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <div v-if="system['enabled_reward']">
 | 
			
		||||
@@ -534,7 +548,6 @@ const onUploadImg = (files, callback) => {
 | 
			
		||||
 | 
			
		||||
          .el-icon {
 | 
			
		||||
            font-size 16px
 | 
			
		||||
            margin-left 10px
 | 
			
		||||
            cursor pointer
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -241,7 +241,7 @@ const editChat = (row) => {
 | 
			
		||||
  tmpChatTitle.value = row.title
 | 
			
		||||
}
 | 
			
		||||
const saveTitle = () => {
 | 
			
		||||
  httpPost('/api/chat/update', {id: item.value.id, title: tmpChatTitle.value}).then(() => {
 | 
			
		||||
  httpPost('/api/chat/update', {chat_id: item.value.chat_id, title: tmpChatTitle.value}).then(() => {
 | 
			
		||||
    showSuccessToast("操作成功!");
 | 
			
		||||
    item.value.title = tmpChatTitle.value;
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,637 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="mobile-mj">
 | 
			
		||||
    <van-form @submit="generate">
 | 
			
		||||
      <div class="text-line">图片比例</div>
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-row :gutter="10">
 | 
			
		||||
          <van-col :span="4" v-for="item in rates" :key="item.value">
 | 
			
		||||
            <div
 | 
			
		||||
                :class="item.value === params.rate ? 'rate active' : 'rate'"
 | 
			
		||||
                @click="changeRate(item)">
 | 
			
		||||
              <div class="icon">
 | 
			
		||||
                <van-image :src="item.img" fit="cover"></van-image>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="text">{{ item.text }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-col>
 | 
			
		||||
        </van-row>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="text-line">模型选择</div>
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-row :gutter="10">
 | 
			
		||||
          <van-col :span="8" v-for="item in models" :key="item.value">
 | 
			
		||||
            <div :class="item.value === params.model ? 'model active' : 'model'"
 | 
			
		||||
                 @click="changeModel(item)">
 | 
			
		||||
              <div class="icon">
 | 
			
		||||
                <van-image :src="item.img" fit="cover"></van-image>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="text">
 | 
			
		||||
                <van-text-ellipsis :content="item.text"/>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-col>
 | 
			
		||||
        </van-row>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-field label="创意度">
 | 
			
		||||
          <template #input>
 | 
			
		||||
            <van-slider v-model.number="params.chaos" :max="100" :step="1"
 | 
			
		||||
                        @update:model-value="showToast('当前值:' + params.chaos)"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-field label="风格化">
 | 
			
		||||
          <template #input>
 | 
			
		||||
            <van-slider v-model.number="params.stylize" :max="1000" :step="1"
 | 
			
		||||
                        @update:model-value="showToast('当前值:' + params.stylize)"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-field label="原始模式">
 | 
			
		||||
          <template #input>
 | 
			
		||||
            <van-switch v-model="params.raw"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-tabs v-model:active="activeName" @change="tabChange" animated>
 | 
			
		||||
          <van-tab title="文生图" name="txt2img">
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-field v-model="params.prompt"
 | 
			
		||||
                         rows="3"
 | 
			
		||||
                         autosize
 | 
			
		||||
                         type="textarea"
 | 
			
		||||
                         placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-tab>
 | 
			
		||||
          <van-tab title="图生图" name="img2img">
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-field v-model="params.prompt"
 | 
			
		||||
                         rows="3"
 | 
			
		||||
                         autosize
 | 
			
		||||
                         type="textarea"
 | 
			
		||||
                         placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-uploader v-model="imgList" :after-read="uploadImg"/>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-field label="垫图权重">
 | 
			
		||||
                <template #input>
 | 
			
		||||
                  <van-slider v-model.number="params.iw" :max="1" :step="0.01"
 | 
			
		||||
                              @update:model-value="showToast('当前值:' + params.iw)"/>
 | 
			
		||||
                </template>
 | 
			
		||||
              </van-field>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="tip-text">提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。</div>
 | 
			
		||||
            <van-cell-group>
 | 
			
		||||
              <van-field
 | 
			
		||||
                  v-model="params.cref"
 | 
			
		||||
                  center
 | 
			
		||||
                  clearable
 | 
			
		||||
                  label="角色一致性"
 | 
			
		||||
                  placeholder="请输入图片URL或者上传图片"
 | 
			
		||||
              >
 | 
			
		||||
                <template #button>
 | 
			
		||||
                  <van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
 | 
			
		||||
                    <van-button size="mini" type="primary" icon="plus"/>
 | 
			
		||||
                  </van-uploader>
 | 
			
		||||
                </template>
 | 
			
		||||
              </van-field>
 | 
			
		||||
            </van-cell-group>
 | 
			
		||||
 | 
			
		||||
            <van-cell-group>
 | 
			
		||||
              <van-field
 | 
			
		||||
                  v-model="params.sref"
 | 
			
		||||
                  center
 | 
			
		||||
                  clearable
 | 
			
		||||
                  label="风格一致性"
 | 
			
		||||
                  placeholder="请输入图片URL或者上传图片"
 | 
			
		||||
              >
 | 
			
		||||
                <template #button>
 | 
			
		||||
                  <van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
 | 
			
		||||
                    <van-button size="mini" type="primary" icon="plus"/>
 | 
			
		||||
                  </van-uploader>
 | 
			
		||||
                </template>
 | 
			
		||||
              </van-field>
 | 
			
		||||
            </van-cell-group>
 | 
			
		||||
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-field label="一致性权重">
 | 
			
		||||
                <template #input>
 | 
			
		||||
                  <van-slider v-model.number="params.cw" :max="100" :step="1"
 | 
			
		||||
                              @update:model-value="showToast('当前值:' + params.cw)"/>
 | 
			
		||||
                </template>
 | 
			
		||||
              </van-field>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-tab>
 | 
			
		||||
          <van-tab title="融图" name="blend">
 | 
			
		||||
            <div class="tip-text">请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。</div>
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-uploader v-model="imgList" :after-read="uploadImg"/>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-tab>
 | 
			
		||||
          <van-tab title="换脸" name="swapFace">
 | 
			
		||||
            <div class="tip-text">请上传两张有脸部的图片,用左边图片的脸替换右边图片的脸。</div>
 | 
			
		||||
            <div class="text-line">
 | 
			
		||||
              <van-uploader v-model="imgList" :after-read="uploadImg"/>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-tab>
 | 
			
		||||
        </van-tabs>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-collapse v-model="activeColspan">
 | 
			
		||||
          <van-collapse-item title="反向提示词" name="neg_prompt">
 | 
			
		||||
            <van-field
 | 
			
		||||
                v-model="params.neg_prompt"
 | 
			
		||||
                rows="3"
 | 
			
		||||
                autosize
 | 
			
		||||
                type="textarea"
 | 
			
		||||
                placeholder="不想出现在图片上的元素(例如:树,建筑)"
 | 
			
		||||
            />
 | 
			
		||||
          </van-collapse-item>
 | 
			
		||||
        </van-collapse>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <el-tag>绘图消耗{{ mjPower }}算力,U/V 操作消耗{{ mjActionPower }}算力,当前算力:{{ power }}</el-tag>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="text-line">
 | 
			
		||||
        <van-button round block type="primary" native-type="submit">
 | 
			
		||||
          立即生成
 | 
			
		||||
        </van-button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </van-form>
 | 
			
		||||
 | 
			
		||||
    <h3>任务列表</h3>
 | 
			
		||||
    <div class="running-job-list">
 | 
			
		||||
      <van-empty v-if="runningJobs.length ===0"
 | 
			
		||||
                 image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
 | 
			
		||||
                 image-size="80"
 | 
			
		||||
                 description="暂无记录"
 | 
			
		||||
      />
 | 
			
		||||
      <van-grid :gutter="10" :column-num="3" v-else>
 | 
			
		||||
        <van-grid-item v-for="item in runningJobs">
 | 
			
		||||
          <div v-if="item.progress > 0">
 | 
			
		||||
            <van-image :src="item['img_url']">
 | 
			
		||||
              <template v-slot:error>加载失败</template>
 | 
			
		||||
            </van-image>
 | 
			
		||||
            <div class="progress">
 | 
			
		||||
              <van-circle
 | 
			
		||||
                  v-model:current-rate="item.progress"
 | 
			
		||||
                  :rate="item.progress"
 | 
			
		||||
                  :speed="100"
 | 
			
		||||
                  :text="item.progress+'%'"
 | 
			
		||||
                  :stroke-width="60"
 | 
			
		||||
                  size="90px"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div v-else class="task-in-queue">
 | 
			
		||||
            <span class="icon"><i class="iconfont icon-quick-start"></i></span>
 | 
			
		||||
            <span class="text">排队中</span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
        </van-grid-item>
 | 
			
		||||
      </van-grid>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <h3>创作记录</h3>
 | 
			
		||||
    <div class="finish-job-list">
 | 
			
		||||
      <van-empty v-if="finishedJobs.length ===0"
 | 
			
		||||
                 image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
 | 
			
		||||
                 image-size="80"
 | 
			
		||||
                 description="暂无记录"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <van-list v-else
 | 
			
		||||
                v-model:error="error"
 | 
			
		||||
                v-model:loading="loading"
 | 
			
		||||
                :finished="finished"
 | 
			
		||||
                error-text="请求失败,点击重新加载"
 | 
			
		||||
                finished-text="没有更多了"
 | 
			
		||||
                @load="onLoad"
 | 
			
		||||
      >
 | 
			
		||||
        <van-grid :gutter="10" :column-num="2">
 | 
			
		||||
          <van-grid-item v-for="item in finishedJobs">
 | 
			
		||||
            <div class="job-item">
 | 
			
		||||
              <van-image
 | 
			
		||||
                  :src="item['thumb_url']"
 | 
			
		||||
                  :class="item['can_opt'] ? '' : 'upscale'"
 | 
			
		||||
                  lazy-load
 | 
			
		||||
                  @click="imageView(item)"
 | 
			
		||||
                  fit="cover">
 | 
			
		||||
                <template v-slot:loading>
 | 
			
		||||
                  <van-loading type="spinner" size="20"/>
 | 
			
		||||
                </template>
 | 
			
		||||
              </van-image>
 | 
			
		||||
 | 
			
		||||
              <div class="opt" v-if="item['can_opt']">
 | 
			
		||||
 | 
			
		||||
                <van-grid :gutter="3" :column-num="4">
 | 
			
		||||
                  <van-grid-item><a @click="upscale(1, item)" class="opt-btn">U1</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item>
 | 
			
		||||
                  <van-grid-item><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item>
 | 
			
		||||
                </van-grid>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="remove">
 | 
			
		||||
                <el-button type="danger" :icon="Delete" @click="removeImage(item)" circle/>
 | 
			
		||||
                <el-button type="warning" v-if="item.publish" @click="publishImage(item, false)"
 | 
			
		||||
                           circle>
 | 
			
		||||
                  <i class="iconfont icon-cancel-share"></i>
 | 
			
		||||
                </el-button>
 | 
			
		||||
                <el-button type="success" v-else @click="publishImage(item, true)" circle>
 | 
			
		||||
                  <i class="iconfont icon-share-bold"></i>
 | 
			
		||||
                </el-button>
 | 
			
		||||
                <el-button type="primary" @click="showPrompt(item)" circle>
 | 
			
		||||
                  <i class="iconfont icon-prompt"></i>
 | 
			
		||||
                </el-button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-grid-item>
 | 
			
		||||
        </van-grid>
 | 
			
		||||
      </van-list>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {nextTick, onMounted, onUnmounted, ref} from "vue";
 | 
			
		||||
import {
 | 
			
		||||
  showConfirmDialog,
 | 
			
		||||
  showFailToast,
 | 
			
		||||
  showNotify,
 | 
			
		||||
  showToast,
 | 
			
		||||
  showDialog,
 | 
			
		||||
  showImagePreview,
 | 
			
		||||
  showSuccessToast
 | 
			
		||||
} from "vant";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import Compressor from "compressorjs";
 | 
			
		||||
import {getSessionId} from "@/store/session";
 | 
			
		||||
import {checkSession} from "@/action/session";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {Delete} from "@element-plus/icons-vue";
 | 
			
		||||
 | 
			
		||||
const activeColspan = ref([""])
 | 
			
		||||
 | 
			
		||||
const rates = [
 | 
			
		||||
  {css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png"},
 | 
			
		||||
  {css: "size2-3", value: "2:3", text: "2:3", img: "/images/mj/rate_3_4.png"},
 | 
			
		||||
  {css: "size3-4", value: "3:4", text: "3:4", img: "/images/mj/rate_3_4.png"},
 | 
			
		||||
  {css: "size4-3", value: "4:3", text: "4:3", img: "/images/mj/rate_4_3.png"},
 | 
			
		||||
  {css: "size16-9", value: "16:9", text: "16:9", img: "/images/mj/rate_16_9.png"},
 | 
			
		||||
  {css: "size9-16", value: "9:16", text: "9:16", img: "/images/mj/rate_9_16.png"},
 | 
			
		||||
]
 | 
			
		||||
const models = [
 | 
			
		||||
  {text: "MJ-6.0", value: " --v 6", img: "/images/mj/mj-v6.png"},
 | 
			
		||||
  {text: "MJ-5.2", value: " --v 5.2", img: "/images/mj/mj-v5.2.png"},
 | 
			
		||||
  {text: "Niji5", value: " --niji 5", img: "/images/mj/mj-niji.png"},
 | 
			
		||||
  {text: "Niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg"},
 | 
			
		||||
  {text: "Niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg"},
 | 
			
		||||
  {text: "Niji6", value: " --niji 6", img: "/images/mj/nj3.jpg"},
 | 
			
		||||
]
 | 
			
		||||
const imgList = ref([])
 | 
			
		||||
const params = ref({
 | 
			
		||||
  task_type: "image",
 | 
			
		||||
  rate: rates[0].value,
 | 
			
		||||
  model: models[0].value,
 | 
			
		||||
  chaos: 0,
 | 
			
		||||
  stylize: 0,
 | 
			
		||||
  seed: 0,
 | 
			
		||||
  img_arr: [],
 | 
			
		||||
  raw: false,
 | 
			
		||||
  iw: 0,
 | 
			
		||||
  prompt: "",
 | 
			
		||||
  neg_prompt: "",
 | 
			
		||||
  tile: false,
 | 
			
		||||
  quality: 0,
 | 
			
		||||
  cref: "",
 | 
			
		||||
  sref: "",
 | 
			
		||||
  cw: 0,
 | 
			
		||||
})
 | 
			
		||||
const userId = ref(0)
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const runningJobs = ref([])
 | 
			
		||||
const finishedJobs = ref([])
 | 
			
		||||
const socket = ref(null)
 | 
			
		||||
const power = ref(0)
 | 
			
		||||
const activeName = ref("txt2img")
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  checkSession().then(user => {
 | 
			
		||||
    power.value = user['power']
 | 
			
		||||
    userId.value = user.id
 | 
			
		||||
 | 
			
		||||
    fetchRunningJobs()
 | 
			
		||||
    fetchFinishJobs(1)
 | 
			
		||||
    connect()
 | 
			
		||||
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    router.push('/login')
 | 
			
		||||
  });
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  socket.value = null
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const mjPower = ref(1)
 | 
			
		||||
const mjActionPower = ref(1)
 | 
			
		||||
httpGet("/api/config/get?key=system").then(res => {
 | 
			
		||||
  mjPower.value = res.data["mj_power"]
 | 
			
		||||
  mjActionPower.value = res.data["mj_action_power"]
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
  showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const heartbeatHandle = ref(null)
 | 
			
		||||
const connect = () => {
 | 
			
		||||
  let host = process.env.VUE_APP_WS_HOST
 | 
			
		||||
  if (host === '') {
 | 
			
		||||
    if (location.protocol === 'https:') {
 | 
			
		||||
      host = 'wss://' + location.host;
 | 
			
		||||
    } else {
 | 
			
		||||
      host = 'ws://' + location.host;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 心跳函数
 | 
			
		||||
  const sendHeartbeat = () => {
 | 
			
		||||
    clearTimeout(heartbeatHandle.value)
 | 
			
		||||
    new Promise((resolve, reject) => {
 | 
			
		||||
      if (socket.value !== null) {
 | 
			
		||||
        socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
 | 
			
		||||
      }
 | 
			
		||||
      resolve("success")
 | 
			
		||||
    }).then(() => {
 | 
			
		||||
      heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const _socket = new WebSocket(host + `/api/mj/client?user_id=${userId.value}`);
 | 
			
		||||
  _socket.addEventListener('open', () => {
 | 
			
		||||
    socket.value = _socket;
 | 
			
		||||
 | 
			
		||||
    // 发送心跳消息
 | 
			
		||||
    sendHeartbeat()
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  _socket.addEventListener('message', event => {
 | 
			
		||||
    if (event.data instanceof Blob) {
 | 
			
		||||
      fetchRunningJobs()
 | 
			
		||||
      fetchFinishJobs(1)
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  _socket.addEventListener('close', () => {
 | 
			
		||||
    if (socket.value !== null) {
 | 
			
		||||
      connect()
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取运行中的任务
 | 
			
		||||
const fetchRunningJobs = (userId) => {
 | 
			
		||||
  httpGet(`/api/mj/jobs?status=0&user_id=${userId}`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
      if (jobs[i].progress === -1) {
 | 
			
		||||
        showNotify({
 | 
			
		||||
          message: `任务执行失败:${jobs[i]['err_msg']}`,
 | 
			
		||||
          type: 'danger',
 | 
			
		||||
        })
 | 
			
		||||
        if (jobs[i].type === 'image') {
 | 
			
		||||
          power.value += mjPower.value
 | 
			
		||||
        } else {
 | 
			
		||||
          power.value += mjActionPower.value
 | 
			
		||||
        }
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
      _jobs.push(jobs[i])
 | 
			
		||||
    }
 | 
			
		||||
    runningJobs.value = _jobs
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showNotify({type: "danger", message: "获取任务失败:" + e.message})
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loading = ref(false)
 | 
			
		||||
const finished = ref(false)
 | 
			
		||||
const error = ref(false)
 | 
			
		||||
const page = ref(0)
 | 
			
		||||
const pageSize = ref(10)
 | 
			
		||||
const fetchFinishJobs = (page) => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  // 获取已完成的任务
 | 
			
		||||
  httpGet(`/api/mj/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
      if (jobs[i].progress === -1) {
 | 
			
		||||
        showNotify({
 | 
			
		||||
          message: `任务ID:${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
 | 
			
		||||
          type: 'danger',
 | 
			
		||||
        })
 | 
			
		||||
        if (jobs[i].type === 'image') {
 | 
			
		||||
          power.value += mjPower.value
 | 
			
		||||
        } else {
 | 
			
		||||
          power.value += mjActionPower.value
 | 
			
		||||
        }
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (jobs[i]['use_proxy']) {
 | 
			
		||||
        jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?x-oss-process=image/quality,q_60&format=webp'
 | 
			
		||||
      } else {
 | 
			
		||||
        if (jobs[i].type === 'upscale' || jobs[i].type === 'swapFace') {
 | 
			
		||||
          jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
 | 
			
		||||
        } else {
 | 
			
		||||
          jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/480/q/75'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (jobs[i].type === 'image' || jobs[i].type === 'variation') {
 | 
			
		||||
        jobs[i]['can_opt'] = true
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (jobs.length < pageSize.value) {
 | 
			
		||||
      finished.value = true
 | 
			
		||||
    }
 | 
			
		||||
    if (page === 1) {
 | 
			
		||||
      finishedJobs.value = jobs
 | 
			
		||||
    } else {
 | 
			
		||||
      finishedJobs.value = finishedJobs.value.concat(jobs)
 | 
			
		||||
    }
 | 
			
		||||
    nextTick(() => loading.value = false)
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    loading.value = false
 | 
			
		||||
    error.value = true
 | 
			
		||||
    showFailToast("获取任务失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const onLoad = () => {
 | 
			
		||||
  page.value += 1
 | 
			
		||||
  fetchFinishJobs(page.value)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 切换图片比例
 | 
			
		||||
const changeRate = (item) => {
 | 
			
		||||
  params.value.rate = item.value
 | 
			
		||||
}
 | 
			
		||||
// 切换模型
 | 
			
		||||
const changeModel = (item) => {
 | 
			
		||||
  params.value.model = item.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const imgKey = ref("")
 | 
			
		||||
const beforeUpload = (key) => {
 | 
			
		||||
  imgKey.value = key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 图片上传
 | 
			
		||||
const uploadImg = (file) => {
 | 
			
		||||
  file.status = "uploading"
 | 
			
		||||
  // 压缩图片并上传
 | 
			
		||||
  new Compressor(file.file, {
 | 
			
		||||
    quality: 0.6,
 | 
			
		||||
    success(result) {
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append('file', result, result.name);
 | 
			
		||||
      // 执行上传操作
 | 
			
		||||
      httpPost('/api/upload', formData).then(res => {
 | 
			
		||||
        file.url = res.data.url
 | 
			
		||||
        if (imgKey.value !== "") { // 单张图片上传
 | 
			
		||||
          params.value[imgKey.value] = res.data.url
 | 
			
		||||
          imgKey.value = ''
 | 
			
		||||
        }
 | 
			
		||||
        file.status = "done"
 | 
			
		||||
      }).catch(e => {
 | 
			
		||||
        file.status = 'failed'
 | 
			
		||||
        file.message = '上传失败'
 | 
			
		||||
        showFailToast("图片上传失败:" + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    error(err) {
 | 
			
		||||
      console.log(err.message);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const send = (url, index, item) => {
 | 
			
		||||
  httpPost(url, {
 | 
			
		||||
    index: index,
 | 
			
		||||
    channel_id: item.channel_id,
 | 
			
		||||
    message_id: item.message_id,
 | 
			
		||||
    message_hash: item.hash,
 | 
			
		||||
    session_id: getSessionId(),
 | 
			
		||||
    prompt: item.prompt,
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    showSuccessToast("任务推送成功,请耐心等待任务执行...")
 | 
			
		||||
    power.value -= mjActionPower.value
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showFailToast("任务推送失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 图片放大任务
 | 
			
		||||
const upscale = (index, item) => {
 | 
			
		||||
  send('/api/mj/upscale', index, item)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 图片变换任务
 | 
			
		||||
const variation = (index, item) => {
 | 
			
		||||
  send('/api/mj/variation', index, item)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generate = () => {
 | 
			
		||||
  if (params.value.prompt === '' && params.value.task_type === "image") {
 | 
			
		||||
    return showFailToast("请输入绘画提示词!")
 | 
			
		||||
  }
 | 
			
		||||
  if (params.value.model.indexOf("niji") !== -1 && params.value.raw) {
 | 
			
		||||
    return showFailToast("动漫模型不允许启用原始模式")
 | 
			
		||||
  }
 | 
			
		||||
  params.value.session_id = getSessionId()
 | 
			
		||||
  params.value.img_arr = imgList.value.map(img => img.url)
 | 
			
		||||
  httpPost("/api/mj/image", params.value).then(() => {
 | 
			
		||||
    showToast("绘画任务推送成功,请耐心等待任务执行")
 | 
			
		||||
    power.value -= mjPower.value
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showFailToast("任务推送失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const removeImage = (item) => {
 | 
			
		||||
  showConfirmDialog({
 | 
			
		||||
    title: '标题',
 | 
			
		||||
    message:
 | 
			
		||||
        '此操作将会删除任务和图片,继续操作码?',
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
      showSuccessToast("任务删除成功")
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      showFailToast("任务删除失败:" + e.message)
 | 
			
		||||
    })
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    showToast("您取消了操作")
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
// 发布图片到作品墙
 | 
			
		||||
const publishImage = (item, action) => {
 | 
			
		||||
  let text = "图片发布"
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
    showSuccessToast(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showFailToast(text + "失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const showPrompt = (item) => {
 | 
			
		||||
  showDialog({
 | 
			
		||||
    title: "绘画提示词",
 | 
			
		||||
    message: item.prompt,
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    // on close
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const imageView = (item) => {
 | 
			
		||||
  showImagePreview([item['img_url']]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 切换菜单
 | 
			
		||||
const tabChange = (tab) => {
 | 
			
		||||
  if (tab === "txt2img" || tab === "img2img") {
 | 
			
		||||
    params.value.task_type = "image"
 | 
			
		||||
  } else {
 | 
			
		||||
    params.value.task_type = tab
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
@import "@/assets/css/mobile/image-mj.styl"
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,523 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="mobile-sd">
 | 
			
		||||
    <van-form @submit="generate">
 | 
			
		||||
      <van-cell-group inset>
 | 
			
		||||
        <div>
 | 
			
		||||
          <van-field
 | 
			
		||||
              v-model="params.sampler"
 | 
			
		||||
              is-link
 | 
			
		||||
              readonly
 | 
			
		||||
              label="采样方法"
 | 
			
		||||
              placeholder="选择采样方法"
 | 
			
		||||
              @click="showSamplerPicker = true"
 | 
			
		||||
          />
 | 
			
		||||
          <van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
 | 
			
		||||
            <van-picker
 | 
			
		||||
                :columns="samplers"
 | 
			
		||||
                @cancel="showSamplerPicker = false"
 | 
			
		||||
                @confirm="samplerConfirm"
 | 
			
		||||
            />
 | 
			
		||||
          </van-popup>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <van-field label="图片尺寸">
 | 
			
		||||
          <template #input>
 | 
			
		||||
            <van-row gutter="20">
 | 
			
		||||
              <van-col span="12">
 | 
			
		||||
                <el-input v-model="params.width" size="small" placeholder="宽"/>
 | 
			
		||||
              </van-col>
 | 
			
		||||
              <van-col span="12">
 | 
			
		||||
                <el-input v-model="params.height" size="small" placeholder="高"/>
 | 
			
		||||
              </van-col>
 | 
			
		||||
            </van-row>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
 | 
			
		||||
        <van-field v-model.number="params.steps" label="迭代步数"
 | 
			
		||||
                   placeholder="">
 | 
			
		||||
          <template #right-icon>
 | 
			
		||||
            <van-icon name="info-o"
 | 
			
		||||
                      @click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
        <van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
 | 
			
		||||
          <template #right-icon>
 | 
			
		||||
            <van-icon name="info-o"
 | 
			
		||||
                      @click="showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
        <van-field v-model.number="params.seed" label="随机因子" placeholder="">
 | 
			
		||||
          <template #right-icon>
 | 
			
		||||
            <van-icon name="info-o"
 | 
			
		||||
                      @click="showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
 | 
			
		||||
        <van-field label="高清修复">
 | 
			
		||||
          <template #input>
 | 
			
		||||
            <van-switch v-model="params.hd_fix"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </van-field>
 | 
			
		||||
 | 
			
		||||
        <div v-if="params.hd_fix">
 | 
			
		||||
          <div>
 | 
			
		||||
            <van-field
 | 
			
		||||
                v-model="params.hd_scale_alg"
 | 
			
		||||
                is-link
 | 
			
		||||
                readonly
 | 
			
		||||
                label="放大算法"
 | 
			
		||||
                placeholder="选择放大算法"
 | 
			
		||||
                @click="showUpscalePicker = true"
 | 
			
		||||
            />
 | 
			
		||||
            <van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
 | 
			
		||||
              <van-picker
 | 
			
		||||
                  :columns="upscaleAlgArr"
 | 
			
		||||
                  @cancel="showUpscalePicker = false"
 | 
			
		||||
                  @confirm="upscaleConfirm"
 | 
			
		||||
              />
 | 
			
		||||
            </van-popup>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <van-field v-model.number="params.hd_scale" label="放大倍数"/>
 | 
			
		||||
          <van-field v-model.number="params.hd_steps" label="迭代步数"/>
 | 
			
		||||
 | 
			
		||||
          <van-field label="重绘幅度">
 | 
			
		||||
            <template #input>
 | 
			
		||||
              <van-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1"
 | 
			
		||||
                          @update:model-value="showToast('当前值:' + params.hd_redraw_rate)"/>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template #right-icon>
 | 
			
		||||
              <van-icon name="info-o"
 | 
			
		||||
                        @click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"/>
 | 
			
		||||
            </template>
 | 
			
		||||
          </van-field>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <van-field
 | 
			
		||||
            v-model="params.prompt"
 | 
			
		||||
            rows="3"
 | 
			
		||||
            autosize
 | 
			
		||||
            type="textarea"
 | 
			
		||||
            placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <van-collapse v-model="activeColspan">
 | 
			
		||||
          <van-collapse-item title="反向提示词" name="neg_prompt">
 | 
			
		||||
            <van-field
 | 
			
		||||
                v-model="params.neg_prompt"
 | 
			
		||||
                rows="3"
 | 
			
		||||
                autosize
 | 
			
		||||
                type="textarea"
 | 
			
		||||
                placeholder="不想出现在图片上的元素(例如:树,建筑)"
 | 
			
		||||
            />
 | 
			
		||||
          </van-collapse-item>
 | 
			
		||||
        </van-collapse>
 | 
			
		||||
 | 
			
		||||
        <div class="text-line pt-6">
 | 
			
		||||
          <el-tag>绘图消耗{{ sdPower }}算力,当前算力:{{ power }}</el-tag>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="text-line">
 | 
			
		||||
          <van-button round block type="primary" native-type="submit">
 | 
			
		||||
            立即生成
 | 
			
		||||
          </van-button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </van-cell-group>
 | 
			
		||||
    </van-form>
 | 
			
		||||
 | 
			
		||||
    <h3>任务列表</h3>
 | 
			
		||||
    <div class="running-job-list">
 | 
			
		||||
      <van-empty v-if="runningJobs.length ===0"
 | 
			
		||||
                 image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
 | 
			
		||||
                 image-size="80"
 | 
			
		||||
                 description="暂无记录"
 | 
			
		||||
      />
 | 
			
		||||
      <van-grid :gutter="10" :column-num="3" v-else>
 | 
			
		||||
        <van-grid-item v-for="item in runningJobs">
 | 
			
		||||
          <div v-if="item.progress > 0">
 | 
			
		||||
            <van-image :src="item['img_url']">
 | 
			
		||||
              <template v-slot:error>加载失败</template>
 | 
			
		||||
            </van-image>
 | 
			
		||||
            <div class="progress">
 | 
			
		||||
              <van-circle
 | 
			
		||||
                  v-model:current-rate="item.progress"
 | 
			
		||||
                  :rate="item.progress"
 | 
			
		||||
                  :speed="100"
 | 
			
		||||
                  :text="item.progress+'%'"
 | 
			
		||||
                  :stroke-width="60"
 | 
			
		||||
                  size="90px"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div v-else class="task-in-queue">
 | 
			
		||||
            <span class="icon"><i class="iconfont icon-quick-start"></i></span>
 | 
			
		||||
            <span class="text">排队中</span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
        </van-grid-item>
 | 
			
		||||
      </van-grid>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <h3>创作记录</h3>
 | 
			
		||||
    <div class="finish-job-list">
 | 
			
		||||
      <van-empty v-if="finishedJobs.length ===0"
 | 
			
		||||
                 image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
 | 
			
		||||
                 image-size="80"
 | 
			
		||||
                 description="暂无记录"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <van-list v-else
 | 
			
		||||
                v-model:error="error"
 | 
			
		||||
                v-model:loading="loading"
 | 
			
		||||
                :finished="finished"
 | 
			
		||||
                error-text="请求失败,点击重新加载"
 | 
			
		||||
                finished-text="没有更多了"
 | 
			
		||||
                @load="onLoad"
 | 
			
		||||
      >
 | 
			
		||||
        <van-grid :gutter="10" :column-num="2">
 | 
			
		||||
          <van-grid-item v-for="item in finishedJobs">
 | 
			
		||||
            <div class="job-item">
 | 
			
		||||
              <van-image
 | 
			
		||||
                  :src="item['img_url']"
 | 
			
		||||
                  :class="item['can_opt'] ? '' : 'upscale'"
 | 
			
		||||
                  lazy-load
 | 
			
		||||
                  @click="imageView(item)"
 | 
			
		||||
                  fit="cover">
 | 
			
		||||
                <template v-slot:loading>
 | 
			
		||||
                  <van-loading type="spinner" size="20"/>
 | 
			
		||||
                </template>
 | 
			
		||||
              </van-image>
 | 
			
		||||
 | 
			
		||||
              <div class="remove">
 | 
			
		||||
                <el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle/>
 | 
			
		||||
                <el-button type="warning" v-if="item.publish" @click="publishImage($event,item, false)"
 | 
			
		||||
                           circle>
 | 
			
		||||
                  <i class="iconfont icon-cancel-share"></i>
 | 
			
		||||
                </el-button>
 | 
			
		||||
                <el-button type="success" v-else @click="publishImage($event, item, true)" circle>
 | 
			
		||||
                  <i class="iconfont icon-share-bold"></i>
 | 
			
		||||
                </el-button>
 | 
			
		||||
                <el-button type="primary" @click="showTask(item)" circle>
 | 
			
		||||
                  <i class="iconfont icon-prompt"></i>
 | 
			
		||||
                </el-button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </van-grid-item>
 | 
			
		||||
        </van-grid>
 | 
			
		||||
      </van-list>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, onUnmounted, ref} from "vue"
 | 
			
		||||
import {Delete} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import Clipboard from "clipboard";
 | 
			
		||||
import {checkSession} from "@/action/session";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {getSessionId} from "@/store/session";
 | 
			
		||||
import {
 | 
			
		||||
  showConfirmDialog, showDialog,
 | 
			
		||||
  showFailToast,
 | 
			
		||||
  showImagePreview,
 | 
			
		||||
  showNotify,
 | 
			
		||||
  showSuccessToast,
 | 
			
		||||
  showToast
 | 
			
		||||
} from "vant";
 | 
			
		||||
 | 
			
		||||
const listBoxHeight = ref(window.innerHeight - 40)
 | 
			
		||||
const mjBoxHeight = ref(window.innerHeight - 150)
 | 
			
		||||
const showTaskDialog = ref(false)
 | 
			
		||||
const item = ref({})
 | 
			
		||||
const showLoginDialog = ref(false)
 | 
			
		||||
const isLogin = ref(false)
 | 
			
		||||
const activeColspan = ref([""])
 | 
			
		||||
 | 
			
		||||
window.onresize = () => {
 | 
			
		||||
  listBoxHeight.value = window.innerHeight - 40
 | 
			
		||||
  mjBoxHeight.value = window.innerHeight - 150
 | 
			
		||||
}
 | 
			
		||||
const samplers = ref([
 | 
			
		||||
  {text: "Euler a", value: "Euler a"},
 | 
			
		||||
  {text: "DPM++ 2S a Karras", value: "DPM++ 2S a Karras"},
 | 
			
		||||
  {text: "DPM++ 2M Karras", value: "DPM++ 2M Karras"},
 | 
			
		||||
  {text: "DPM++ 2M SDE Karras", value: "DPM++ 2M SDE Karras"},
 | 
			
		||||
  {text: "DPM++ 2M Karras", value: "DPM++ 2M Karras"},
 | 
			
		||||
  {text: "DPM++ 3M SDE Karras", value: "DPM++ 3M SDE Karras"},
 | 
			
		||||
])
 | 
			
		||||
const showSamplerPicker = ref(false)
 | 
			
		||||
 | 
			
		||||
const upscaleAlgArr = ref([
 | 
			
		||||
  {text: "Latent", value: "Latent"},
 | 
			
		||||
  {text: "ESRGAN_4x", value: "ESRGAN_4x"},
 | 
			
		||||
  {text: "ESRGAN 4x+", value: "ESRGAN 4x+"},
 | 
			
		||||
  {text: "SwinIR_4x", value: "SwinIR_4x"},
 | 
			
		||||
  {text: "LDSR", value: "LDSR"},
 | 
			
		||||
])
 | 
			
		||||
const showUpscalePicker = ref(false)
 | 
			
		||||
 | 
			
		||||
const params = ref({
 | 
			
		||||
  width: 1024,
 | 
			
		||||
  height: 1024,
 | 
			
		||||
  sampler: samplers.value[0].value,
 | 
			
		||||
  seed: -1,
 | 
			
		||||
  steps: 20,
 | 
			
		||||
  cfg_scale: 7,
 | 
			
		||||
  hd_fix: false,
 | 
			
		||||
  hd_redraw_rate: 0.7,
 | 
			
		||||
  hd_scale: 2,
 | 
			
		||||
  hd_scale_alg: upscaleAlgArr.value[0].value,
 | 
			
		||||
  hd_steps: 0,
 | 
			
		||||
  prompt: "",
 | 
			
		||||
  neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const runningJobs = ref([])
 | 
			
		||||
const finishedJobs = ref([])
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
// 检查是否有画同款的参数
 | 
			
		||||
const _params = router.currentRoute.value.params["copyParams"]
 | 
			
		||||
if (_params) {
 | 
			
		||||
  params.value = JSON.parse(_params)
 | 
			
		||||
}
 | 
			
		||||
const power = ref(0)
 | 
			
		||||
const sdPower = ref(0) // 画一张 SD 图片消耗算力
 | 
			
		||||
 | 
			
		||||
const socket = ref(null)
 | 
			
		||||
const userId = ref(0)
 | 
			
		||||
const heartbeatHandle = ref(null)
 | 
			
		||||
const connect = () => {
 | 
			
		||||
  let host = process.env.VUE_APP_WS_HOST
 | 
			
		||||
  if (host === '') {
 | 
			
		||||
    if (location.protocol === 'https:') {
 | 
			
		||||
      host = 'wss://' + location.host;
 | 
			
		||||
    } else {
 | 
			
		||||
      host = 'ws://' + location.host;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 心跳函数
 | 
			
		||||
  const sendHeartbeat = () => {
 | 
			
		||||
    clearTimeout(heartbeatHandle.value)
 | 
			
		||||
    new Promise((resolve, reject) => {
 | 
			
		||||
      if (socket.value !== null) {
 | 
			
		||||
        socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
 | 
			
		||||
      }
 | 
			
		||||
      resolve("success")
 | 
			
		||||
    }).then(() => {
 | 
			
		||||
      heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const _socket = new WebSocket(host + `/api/sd/client?user_id=${userId.value}`);
 | 
			
		||||
  _socket.addEventListener('open', () => {
 | 
			
		||||
    socket.value = _socket;
 | 
			
		||||
 | 
			
		||||
    // 发送心跳消息
 | 
			
		||||
    sendHeartbeat()
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  _socket.addEventListener('message', event => {
 | 
			
		||||
    if (event.data instanceof Blob) {
 | 
			
		||||
      fetchRunningJobs()
 | 
			
		||||
      finished.value = false
 | 
			
		||||
      page.value = 1
 | 
			
		||||
      fetchFinishJobs(page.value)
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  _socket.addEventListener('close', () => {
 | 
			
		||||
    if (socket.value !== null) {
 | 
			
		||||
      connect()
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const clipboard = ref(null)
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  initData()
 | 
			
		||||
  clipboard.value = new Clipboard('.copy-prompt-sd');
 | 
			
		||||
  clipboard.value.on('success', () => {
 | 
			
		||||
    showNotify({type: "success", message: "复制成功!"});
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  clipboard.value.on('error', () => {
 | 
			
		||||
    showNotify({type: "danger", message: '复制失败!'});
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  httpGet("/api/config/get?key=system").then(res => {
 | 
			
		||||
    sdPower.value = res.data["sd_power"]
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  clipboard.value.destroy()
 | 
			
		||||
  socket.value = null
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const initData = () => {
 | 
			
		||||
  checkSession().then(user => {
 | 
			
		||||
    power.value = user['power']
 | 
			
		||||
    userId.value = user.id
 | 
			
		||||
    isLogin.value = true
 | 
			
		||||
    fetchRunningJobs()
 | 
			
		||||
    fetchFinishJobs(1)
 | 
			
		||||
    connect()
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fetchRunningJobs = () => {
 | 
			
		||||
  // 获取运行中的任务
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=0`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
      if (jobs[i].progress === -1) {
 | 
			
		||||
        showNotify({
 | 
			
		||||
          message: `任务ID:${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
 | 
			
		||||
          type: 'danger',
 | 
			
		||||
        })
 | 
			
		||||
        power.value += sdPower.value
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
      _jobs.push(jobs[i])
 | 
			
		||||
    }
 | 
			
		||||
    runningJobs.value = _jobs
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showNotify({type: "danger", message: "获取任务失败:" + e.message})
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loading = ref(false)
 | 
			
		||||
const finished = ref(false)
 | 
			
		||||
const error = ref(false)
 | 
			
		||||
const page = ref(0)
 | 
			
		||||
const pageSize = ref(10)
 | 
			
		||||
// 获取已完成的任务
 | 
			
		||||
const fetchFinishJobs = (page) => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    if (res.data.length < pageSize.value) {
 | 
			
		||||
      finished.value = true
 | 
			
		||||
    }
 | 
			
		||||
    if (page === 1) {
 | 
			
		||||
      finishedJobs.value = res.data
 | 
			
		||||
    } else {
 | 
			
		||||
      finishedJobs.value = finishedJobs.value.concat(res.data)
 | 
			
		||||
    }
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    loading.value = false
 | 
			
		||||
    showNotify({type: "danger", message: "获取任务失败:" + e.message})
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const onLoad = () => {
 | 
			
		||||
  page.value += 1
 | 
			
		||||
  fetchFinishJobs(page.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 创建绘图任务
 | 
			
		||||
const promptRef = ref(null)
 | 
			
		||||
const generate = () => {
 | 
			
		||||
  if (params.value.prompt === '') {
 | 
			
		||||
    promptRef.value.focus()
 | 
			
		||||
    return showToast("请输入绘画提示词!")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!isLogin.value) {
 | 
			
		||||
    showLoginDialog.value = true
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (params.value.seed === '') {
 | 
			
		||||
    params.value.seed = -1
 | 
			
		||||
  }
 | 
			
		||||
  params.value.session_id = getSessionId()
 | 
			
		||||
  httpPost("/api/sd/image", params.value).then(() => {
 | 
			
		||||
    showSuccessToast("绘画任务推送成功,请耐心等待任务执行...")
 | 
			
		||||
    power.value -= sdPower.value
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showFailToast("任务推送失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const showTask = (row) => {
 | 
			
		||||
  item.value = row
 | 
			
		||||
  showTaskDialog.value = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const copyParams = (row) => {
 | 
			
		||||
  params.value = row.params
 | 
			
		||||
  showTaskDialog.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const removeImage = (event, item) => {
 | 
			
		||||
  event.stopPropagation()
 | 
			
		||||
  showConfirmDialog({
 | 
			
		||||
    title: '标题',
 | 
			
		||||
    message:
 | 
			
		||||
        '此操作将会删除任务和图片,继续操作码?',
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
      showSuccessToast("任务删除成功")
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      showFailToast("任务删除失败:" + e.message)
 | 
			
		||||
    })
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    showToast("您取消了操作")
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 发布图片到作品墙
 | 
			
		||||
const publishImage = (event, item, action) => {
 | 
			
		||||
  event.stopPropagation()
 | 
			
		||||
  let text = "图片发布"
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
    showSuccessToast(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showFailToast(text + "失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const imageView = (item) => {
 | 
			
		||||
  showImagePreview([item['img_url']]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const samplerConfirm = (item) => {
 | 
			
		||||
  params.value.sampler = item.selectedOptions[0].text;
 | 
			
		||||
  showSamplerPicker.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const upscaleConfirm = (item) => {
 | 
			
		||||
  params.value.hd_scale_alg = item.selectedOptions[0].text;
 | 
			
		||||
  showUpscalePicker.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const showInfo = (message) => {
 | 
			
		||||
  showDialog({
 | 
			
		||||
    title: "参数说明",
 | 
			
		||||
    message: message,
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    // on close
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
@import "@/assets/css/mobile/image-sd.styl"
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,133 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="img-wall container">
 | 
			
		||||
    <van-nav-bar :title="title"/>
 | 
			
		||||
 | 
			
		||||
    <div class="content">
 | 
			
		||||
      <van-tabs v-model:active="activeName">
 | 
			
		||||
        <van-tab title="MidJourney" name="mj">
 | 
			
		||||
          <van-list
 | 
			
		||||
              v-model:error="data['mj'].error"
 | 
			
		||||
              v-model:loading="data['mj'].loading"
 | 
			
		||||
              :finished="data['mj'].finished"
 | 
			
		||||
              error-text="请求失败,点击重新加载"
 | 
			
		||||
              finished-text="没有更多了"
 | 
			
		||||
              @load="onLoad"
 | 
			
		||||
              style="height: 100%;width: 100%;"
 | 
			
		||||
          >
 | 
			
		||||
            <van-cell v-for="item in data['mj'].data" :key="item.id">
 | 
			
		||||
              <van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/>
 | 
			
		||||
            </van-cell>
 | 
			
		||||
          </van-list>
 | 
			
		||||
        </van-tab>
 | 
			
		||||
        <van-tab title="StableDiffusion" name="sd">
 | 
			
		||||
          <van-list
 | 
			
		||||
              v-model:error="data['sd'].error"
 | 
			
		||||
              v-model:loading="data['sd'].loading"
 | 
			
		||||
              :finished="data['sd'].finished"
 | 
			
		||||
              error-text="请求失败,点击重新加载"
 | 
			
		||||
              finished-text="没有更多了"
 | 
			
		||||
              @load="onLoad"
 | 
			
		||||
          >
 | 
			
		||||
            <van-cell v-for="item in data['sd'].data" :key="item.id">
 | 
			
		||||
              <van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/>
 | 
			
		||||
            </van-cell>
 | 
			
		||||
          </van-list>
 | 
			
		||||
        </van-tab>
 | 
			
		||||
        <van-tab title="DALLE3" name="dalle3">
 | 
			
		||||
          <van-empty description="功能正在开发中"/>
 | 
			
		||||
        </van-tab>
 | 
			
		||||
      </van-tabs>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {showDialog, showFailToast, showSuccessToast} from "vant";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
 | 
			
		||||
const title = ref('图片创作广场')
 | 
			
		||||
const activeName = ref("mj")
 | 
			
		||||
const data = ref({
 | 
			
		||||
  "mj": {
 | 
			
		||||
    loading: false,
 | 
			
		||||
    finished: false,
 | 
			
		||||
    error: false,
 | 
			
		||||
    page: 1,
 | 
			
		||||
    pageSize: 12,
 | 
			
		||||
    url: "/api/mj/jobs",
 | 
			
		||||
    data: []
 | 
			
		||||
  },
 | 
			
		||||
  "sd": {
 | 
			
		||||
    loading: false,
 | 
			
		||||
    finished: false,
 | 
			
		||||
    error: false,
 | 
			
		||||
    page: 1,
 | 
			
		||||
    pageSize: 12,
 | 
			
		||||
    url: "/api/sd/jobs",
 | 
			
		||||
    data: []
 | 
			
		||||
  },
 | 
			
		||||
  "dalle3": {
 | 
			
		||||
    loading: false,
 | 
			
		||||
    finished: false,
 | 
			
		||||
    error: false,
 | 
			
		||||
    page: 1,
 | 
			
		||||
    pageSize: 12,
 | 
			
		||||
    url: "/api/dalle3/jobs",
 | 
			
		||||
    data: []
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const onLoad = () => {
 | 
			
		||||
  const d = data.value[activeName.value]
 | 
			
		||||
  httpGet(`${d.url}?status=1&page=${d.page}&page_size=${d.pageSize}&publish=true`).then(res => {
 | 
			
		||||
    d.loading = false
 | 
			
		||||
    if (res.data.length === 0) {
 | 
			
		||||
      d.finished = true
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 生成缩略图
 | 
			
		||||
    const imageList = res.data
 | 
			
		||||
    for (let i = 0; i < imageList.length; i++) {
 | 
			
		||||
      imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"
 | 
			
		||||
    }
 | 
			
		||||
    if (imageList.length < d.pageSize) {
 | 
			
		||||
      d.finished = true
 | 
			
		||||
    }
 | 
			
		||||
    if (d.data.length === 0) {
 | 
			
		||||
      d.data = imageList
 | 
			
		||||
    } else {
 | 
			
		||||
      d.data = d.data.concat(imageList)
 | 
			
		||||
    }
 | 
			
		||||
    d.page += 1
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    d.error = true
 | 
			
		||||
    showFailToast("加载图片数据失败")
 | 
			
		||||
  })
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showPrompt = (item) => {
 | 
			
		||||
  showDialog({
 | 
			
		||||
    title: "绘画提示词",
 | 
			
		||||
    message: item.prompt,
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    // on close
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
.img-wall {
 | 
			
		||||
  .content {
 | 
			
		||||
    padding-top 60px
 | 
			
		||||
 | 
			
		||||
    .van-cell__value {
 | 
			
		||||
      .van-image {
 | 
			
		||||
        width 100%
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -302,7 +302,7 @@ const pay = (payWay, item) => {
 | 
			
		||||
    if (isWeChatBrowser() && payWay === 'wechat') {
 | 
			
		||||
      showFailToast("请在系统自带浏览器打开支付页面,或者在 PC 端进行扫码支付")
 | 
			
		||||
    } else {
 | 
			
		||||
      location.href = res.data
 | 
			
		||||
      location.href = res.data.url
 | 
			
		||||
    }
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showFailToast("生成支付订单失败:" + e.message)
 | 
			
		||||
 
 | 
			
		||||
@@ -317,7 +317,7 @@ const initData = () => {
 | 
			
		||||
 | 
			
		||||
const fetchRunningJobs = () => {
 | 
			
		||||
  // 获取运行中的任务
 | 
			
		||||
  httpGet(`/api/dall/jobs?status=0`).then(res => {
 | 
			
		||||
  httpGet(`/api/dall/jobs?finish=0`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
@@ -345,7 +345,7 @@ const pageSize = ref(10)
 | 
			
		||||
// 获取已完成的任务
 | 
			
		||||
const fetchFinishJobs = (page) => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  httpGet(`/api/dall/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
  httpGet(`/api/dall/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    if (res.data.length < pageSize.value) {
 | 
			
		||||
      finished.value = true
 | 
			
		||||
    }
 | 
			
		||||
@@ -410,7 +410,7 @@ const removeImage = (event, item) => {
 | 
			
		||||
    message:
 | 
			
		||||
        '此操作将会删除任务和图片,继续操作码?',
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
    httpGet("/api/dall/remove", {id: item.id, user_id: item.user_id}).then(() => {
 | 
			
		||||
      showSuccessToast("任务删除成功")
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      showFailToast("任务删除失败:" + e.message)
 | 
			
		||||
@@ -427,7 +427,7 @@ const publishImage = (event, item, action) => {
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
  httpGet("/api/dall/publish", {id: item.id, action: action, user_id: item.user_id}).then(() => {
 | 
			
		||||
    showSuccessToast(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
 
 | 
			
		||||
@@ -421,7 +421,7 @@ const connect = () => {
 | 
			
		||||
 | 
			
		||||
// 获取运行中的任务
 | 
			
		||||
const fetchRunningJobs = (userId) => {
 | 
			
		||||
  httpGet(`/api/mj/jobs?status=0&user_id=${userId}`).then(res => {
 | 
			
		||||
  httpGet(`/api/mj/jobs?finish=0&user_id=${userId}`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
@@ -453,7 +453,7 @@ const pageSize = ref(10)
 | 
			
		||||
const fetchFinishJobs = (page) => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  // 获取已完成的任务
 | 
			
		||||
  httpGet(`/api/mj/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
  httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
      if (jobs[i].progress === -1) {
 | 
			
		||||
@@ -600,7 +600,7 @@ const removeImage = (item) => {
 | 
			
		||||
    message:
 | 
			
		||||
        '此操作将会删除任务和图片,继续操作码?',
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
    httpGet("/api/mj/remove", {id: item.id, user_id: item.user_id}).then(() => {
 | 
			
		||||
      showSuccessToast("任务删除成功")
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      showFailToast("任务删除失败:" + e.message)
 | 
			
		||||
@@ -615,7 +615,7 @@ const publishImage = (item, action) => {
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
  httpGet("/api/mj/publish", {id: item.id, action: action,user_id: item.user_id}).then(() => {
 | 
			
		||||
    showSuccessToast(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
 
 | 
			
		||||
@@ -381,7 +381,7 @@ const initData = () => {
 | 
			
		||||
 | 
			
		||||
const fetchRunningJobs = () => {
 | 
			
		||||
  // 获取运行中的任务
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=0`).then(res => {
 | 
			
		||||
  httpGet(`/api/sd/jobs?finish=0`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
    const _jobs = []
 | 
			
		||||
    for (let i = 0; i < jobs.length; i++) {
 | 
			
		||||
@@ -409,7 +409,7 @@ const pageSize = ref(10)
 | 
			
		||||
// 获取已完成的任务
 | 
			
		||||
const fetchFinishJobs = (page) => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
  httpGet(`/api/sd/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
 | 
			
		||||
    if (res.data.length < pageSize.value) {
 | 
			
		||||
      finished.value = true
 | 
			
		||||
    }
 | 
			
		||||
@@ -474,7 +474,7 @@ const removeImage = (event, item) => {
 | 
			
		||||
    message:
 | 
			
		||||
        '此操作将会删除任务和图片,继续操作码?',
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
 | 
			
		||||
    httpGet("/api/sd/remove", {id: item.id, user_id: item.user}).then(() => {
 | 
			
		||||
      showSuccessToast("任务删除成功")
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      showFailToast("任务删除失败:" + e.message)
 | 
			
		||||
@@ -491,7 +491,7 @@ const publishImage = (event, item, action) => {
 | 
			
		||||
  if (action === false) {
 | 
			
		||||
    text = "取消发布"
 | 
			
		||||
  }
 | 
			
		||||
  httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
 | 
			
		||||
  httpGet("/api/sd/publish", {id: item.id, action: action, user_id: item.user}).then(() => {
 | 
			
		||||
    showSuccessToast(text + "成功")
 | 
			
		||||
    item.publish = action
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
 
 | 
			
		||||