mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-22 11:04:26 +08:00
merge v4.2.5
This commit is contained in:
@@ -6,7 +6,7 @@ VITE_ADMIN_USER=admin
|
||||
VITE_ADMIN_PASS=admin123
|
||||
VITE_KEY_PREFIX=GeekAI_DEV_
|
||||
VITE_TITLE="Geek-AI 创作系统"
|
||||
VITE_VERSION=v4.2.4
|
||||
VITE_VERSION=v4.2.5
|
||||
VITE_DOCS_URL=https://docs.geekai.me
|
||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
VITE_API_HOST=
|
||||
VITE_WS_HOST=
|
||||
VITE_KEY_PREFIX=GeekAI_
|
||||
VITE_VERSION=v4.2.4
|
||||
VITE_VERSION=v4.2.5
|
||||
VUE_APP_TITLE="Geek-AI 创作系统"
|
||||
VITE_DOCS_URL=https://docs.geekai.me
|
||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
|
||||
@@ -91,7 +91,7 @@ html, body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
--primary-color: #21aa93
|
||||
// --primary-color: #21aa93
|
||||
|
||||
h1 { font-size: 2em; } /* 通常是 2em */
|
||||
h2 { font-size: 1.5em; } /* 通常是 1.5em */
|
||||
@@ -118,6 +118,18 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper.is-customized {
|
||||
/* 设置内边距以保证高度为32px */
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(180deg, #e1bee7, #7e57c2);
|
||||
color #fff
|
||||
}
|
||||
|
||||
.el-popper.is-customized .el-popper__arrow::before {
|
||||
background: linear-gradient(180deg, #b39ddb, #7e57c2);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* 省略显示 */
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
|
||||
349
web/src/assets/css/jimeng.styl
Normal file
349
web/src/assets/css/jimeng.styl
Normal file
@@ -0,0 +1,349 @@
|
||||
.page-jimeng {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--chat-bg);
|
||||
|
||||
// 左侧参数面板
|
||||
.params-panel {
|
||||
min-width: 380px;
|
||||
max-width: 380px;
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: var(--card-bg);
|
||||
box-shadow: var(--card-shadow, 0 8px 24px rgba(0,0,0,0.12));
|
||||
color: var(--text-theme-color);
|
||||
font-size: 14px;
|
||||
overflow: auto;
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
// 功能分类按钮组
|
||||
.category-buttons {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.category-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: var(--primary-color, #5865f2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
|
||||
.category-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px 10px;
|
||||
border: 2px solid var(--border-color, #f0f0f0);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--card-bg-secondary, #fafafa);
|
||||
/* 暗色主题支持 */
|
||||
[data-theme="dark"] & {
|
||||
background: var(--card-bg-secondary-dark, #23242a);
|
||||
border-color: var(--border-color-dark, #33343a);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color, #5865f2);
|
||||
background: var(--card-bg-hover, #f8f9ff);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--card-bg-hover-dark, #2a2b31);
|
||||
}
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color, #5865f2);
|
||||
background: var(--primary-gradient, linear-gradient(135deg, #5865f2 0%, #7289da 100%));
|
||||
color: var(--primary-text-on-primary, #fff);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--primary-gradient-dark, linear-gradient(135deg, #23242a 0%, #2a2b31 100%));
|
||||
color: var(--primary-text-on-primary-dark, #fff);
|
||||
}
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--primary-shadow, 0 4px 12px rgba(88,101,242,0.3));
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 功能开关
|
||||
.function-switch {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.switch-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: var(--primary-color, #5865f2);
|
||||
}
|
||||
}
|
||||
|
||||
.switch-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 15px;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
background: var(--card-bg-secondary, #f9f9f9);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--card-bg-secondary-dark, #23242a);
|
||||
border-color: var(--border-color-dark, #33343a);
|
||||
}
|
||||
|
||||
.switch-info {
|
||||
flex: 1;
|
||||
|
||||
.switch-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.switch-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-sub-color, #666);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 参数容器
|
||||
.params-container {
|
||||
.function-panel {
|
||||
.param-line {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&.pt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
.item-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.label {
|
||||
margin-right: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-info {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: var(--info-bg, #f0f8ff);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--primary-color, #5865f2);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 30px;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧主要内容区域
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: var(--chat-bg);
|
||||
color: var(--text-theme-color);
|
||||
|
||||
.works-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.h-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
.task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1));
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 24px rgba(88,101,242,0.12);
|
||||
}
|
||||
.task-left {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
.task-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 1.2/1;
|
||||
min-height: 220px;
|
||||
max-height: 320px;
|
||||
background: var(--card-bg-secondary, #f0f0f0);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.preview-image, .preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--text-disabled-color, #999);
|
||||
font-size: 16px;
|
||||
.el-icon, .iconfont {
|
||||
font-size: 32px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task-center {
|
||||
flex: none;
|
||||
padding: 18px 18px 8px 18px;
|
||||
.task-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.task-prompt {
|
||||
font-size: 14px;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
.task-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
color: var(--text-disabled-color, #999);
|
||||
}
|
||||
}
|
||||
.task-right {
|
||||
flex: none;
|
||||
padding: 0 18px 16px 18px;
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-jimeng {
|
||||
flex-direction: column;
|
||||
|
||||
.params-panel {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.task-list .task-item {
|
||||
min-height: 320px;
|
||||
.task-left .task-preview {
|
||||
min-height: 160px;
|
||||
max-height: 220px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
.page-keling
|
||||
display flex
|
||||
min-height 100vh
|
||||
:deep(.el-form-item__label)
|
||||
color var(--text-theme-color)
|
||||
.grid-content
|
||||
// background-color #383838
|
||||
background var(--card-bg)
|
||||
border-radius 8px
|
||||
padding 8px 14px
|
||||
display flex
|
||||
cursor pointer
|
||||
margin-bottom 10px
|
||||
// border 1px solid #383838
|
||||
border 1px solid var(--chat-bg)
|
||||
&:hover
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.icon
|
||||
width 20px
|
||||
height 20px
|
||||
margin-bottom 5px
|
||||
.texts
|
||||
margin-left 5px
|
||||
margin-top 2px
|
||||
color var(--text-theme-color)
|
||||
.param-line.pt
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
.grid-content.active
|
||||
// color #47fff1
|
||||
// background-color #585858
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.h-20
|
||||
height 4rem !important
|
||||
.main-content
|
||||
padding-right 1.5rem
|
||||
padding-left 1.5rem
|
||||
padding-bottom 1rem
|
||||
flex 1
|
||||
background var(--chat-bg)
|
||||
// width: 100%;
|
||||
// padding 0 10px 10px 10px
|
||||
color var(--text-theme-color)
|
||||
overflow-x hidden
|
||||
.camera-control
|
||||
padding 10px
|
||||
border-radius 4px
|
||||
background var(--card-bg)
|
||||
:deep(.el-form-item:last-child)
|
||||
margin-bottom 0 !important
|
||||
.title-tabs
|
||||
:deep(.el-tabs__item.is-active)
|
||||
color var(--theme-textcolor-normal)
|
||||
font-size 18px
|
||||
:deep(.el-tabs__item)
|
||||
color var(--text-theme-color)
|
||||
font-size 18px
|
||||
.el-tabs
|
||||
--el-tabs-header-height 55px
|
||||
.el-tabs__item
|
||||
color var(--text-theme-color)
|
||||
font-size 18px
|
||||
.el-tabs__item.is-active, .title-tabs .el-tabs__item.is-active
|
||||
.title-tabs .el-tabs__active-bar
|
||||
background-color var(--theme-textcolor-normal)
|
||||
:deep(.el-textarea)
|
||||
--el-input-focus-border-color var(--el-color-primary)
|
||||
:deep(.el-textarea__inner)
|
||||
background transparent
|
||||
color var(--text-theme-color)
|
||||
.el-input__wrapper
|
||||
background transparent
|
||||
padding 5px
|
||||
.text
|
||||
margin-bottom 10px
|
||||
color #6b778c
|
||||
font-size 15px
|
||||
.param-line.pt
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
.form-item-inner
|
||||
display flex
|
||||
align-items center
|
||||
.el-icon
|
||||
margin-left 10px
|
||||
.el-form-item__label
|
||||
color var(--text-theme-color)
|
||||
// 图片上传样式
|
||||
.img-inline
|
||||
display flex
|
||||
gap 20px
|
||||
align-items center
|
||||
.img-uploader
|
||||
text-align center
|
||||
:deep(.el-upload)
|
||||
border 1px dashed var(--el-border-color)
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
position relative
|
||||
overflow hidden
|
||||
width 120px
|
||||
height 120px
|
||||
line-height 120px
|
||||
transition var(--el-transition-duration-fast)
|
||||
margin-bottom 20px
|
||||
&:hover
|
||||
border-color var(--el-color-primary)
|
||||
.el-icon.uploader-icon
|
||||
font-size 28px
|
||||
color #8c939d
|
||||
width 100%
|
||||
height 120px
|
||||
text-align center
|
||||
.img-list-box
|
||||
display flex
|
||||
.img-item
|
||||
width 120px
|
||||
position relative
|
||||
margin-right 10px
|
||||
.el-image
|
||||
width 120px
|
||||
height 120px
|
||||
border-radius 5px
|
||||
.el-button
|
||||
position absolute
|
||||
right 5px
|
||||
top 5px
|
||||
width 20px
|
||||
height 20px
|
||||
.el-row.text-info
|
||||
width 100%
|
||||
padding 10px 0
|
||||
.el-tag
|
||||
margin-right 10px
|
||||
// 提交按钮
|
||||
.submit-btn
|
||||
display flex
|
||||
margin 20px 0
|
||||
.el-button
|
||||
width 200px
|
||||
.video-list
|
||||
.btn
|
||||
margin-right 10px
|
||||
border none
|
||||
border-radius 5px
|
||||
padding 5px 10px
|
||||
cursor pointer
|
||||
color var(--theme-text-color-primary)
|
||||
background-color var(--btn-bg)
|
||||
&:hover
|
||||
opacity 0.7
|
||||
.list-box
|
||||
padding 0
|
||||
.item
|
||||
display flex
|
||||
flex-flow row
|
||||
align-items center
|
||||
min-height 100px
|
||||
padding 10px 15px
|
||||
border-radius 10px
|
||||
cursor pointer
|
||||
margin-bottom 20px
|
||||
background var(--chat-bg)
|
||||
.left
|
||||
.container
|
||||
width 160px
|
||||
position relative
|
||||
max-height 120px
|
||||
overflow hidden
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
.video
|
||||
width 160px
|
||||
border-radius 5px
|
||||
.el-image
|
||||
width 160px
|
||||
height 90px
|
||||
border-radius 5px
|
||||
.duration
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
background-color rgba(14, 8, 8, 0.7)
|
||||
padding 0 3px
|
||||
font-family 'Input Sans'
|
||||
font-size 14px
|
||||
font-weight 700
|
||||
border-radius 0.125rem
|
||||
.play
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 50%
|
||||
border none
|
||||
border-radius 5px
|
||||
background rgba(100, 100, 100, 0.3)
|
||||
cursor pointer
|
||||
color var(--text-theme-color)
|
||||
opacity 0
|
||||
transform translate(-50%, 0px)
|
||||
transition opacity 0.3s ease 0s
|
||||
&:hover
|
||||
.play
|
||||
opacity 1
|
||||
// display block
|
||||
.center
|
||||
width 100%
|
||||
// border 1px solid saddlebrown
|
||||
display flex
|
||||
justify-content center
|
||||
align-items flex-start
|
||||
flex-flow column
|
||||
padding 0 20px
|
||||
.prompt, .failed
|
||||
padding 0
|
||||
font-size 16px
|
||||
max-height 60px
|
||||
line-height 28px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
.prompt
|
||||
color var(--text-fb)
|
||||
cursor text
|
||||
.failed
|
||||
color #E4696B
|
||||
.right
|
||||
display flex
|
||||
justify-content right
|
||||
min-width 200px
|
||||
font-size 14px
|
||||
padding 0
|
||||
.tools
|
||||
display flex
|
||||
justify-content left
|
||||
align-items center
|
||||
flex-flow row
|
||||
height 90px
|
||||
.btn-publish
|
||||
padding 2px 10px
|
||||
.text
|
||||
margin-right 10px
|
||||
.btn-icon
|
||||
background none
|
||||
padding 6px
|
||||
transition background 0.6s ease 0s
|
||||
color #919191
|
||||
&:hover
|
||||
// background #5f5958
|
||||
// color #e1e1e1
|
||||
color var(--el-color-primary)
|
||||
.downloading
|
||||
width 16px
|
||||
.pagination
|
||||
margin-top 20px
|
||||
display flex
|
||||
justify-content center
|
||||
.inner
|
||||
display flex
|
||||
width 100%
|
||||
.mj-box
|
||||
margin 10px
|
||||
// background-color #262626
|
||||
// border 1px solid #454545
|
||||
// height: calc(100vh - 50px)
|
||||
// overflow: scroll
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 20px
|
||||
border-radius 10px
|
||||
color var(--text-theme-color)
|
||||
font-size 14px
|
||||
overflow auto
|
||||
h2
|
||||
font-weight bold
|
||||
font-size 20px
|
||||
text-align center
|
||||
color var(--theme-textcolor-normal)
|
||||
// 隐藏滚动条
|
||||
::-webkit-scrollbar
|
||||
width 0
|
||||
height 0
|
||||
background-color transparent
|
||||
.mj-params
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
.param-line
|
||||
padding 0 10px
|
||||
.el-icon
|
||||
position relative
|
||||
.model
|
||||
background var(--card-bg)
|
||||
// border 1px solid #454545
|
||||
border-radius 8px
|
||||
padding 5px
|
||||
margin-bottom 10px
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
cursor pointer
|
||||
border 1px solid var(--chat-bg)
|
||||
&:hover
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.el-image
|
||||
height 40px
|
||||
width 100%
|
||||
.text
|
||||
margin-top 4px
|
||||
font-size 12px
|
||||
.model.active
|
||||
// color #47fff1
|
||||
// background-color #585858
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.form-item-inner
|
||||
display flex
|
||||
align-items center
|
||||
.el-select
|
||||
--el-select-input-focus-border-color var(--el-color-primary)
|
||||
--el-input-focus-border-color var(--el-color-primary)
|
||||
.el-input__wrapper
|
||||
background var(--chat-bg)
|
||||
.el-input__inner
|
||||
color var(--text-theme-color)
|
||||
.el-icon
|
||||
margin-left 10px
|
||||
.img-uploader
|
||||
.el-upload
|
||||
border 1px dashed var(--el-border-color)
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
position relative
|
||||
overflow hidden
|
||||
width 100%
|
||||
transition var(--el-transition-duration-fast)
|
||||
&:hover
|
||||
border-color var(--el-color-primary)
|
||||
.el-icon.uploader-icon
|
||||
font-size 28px
|
||||
color #8c939d
|
||||
width 100%
|
||||
height 120px
|
||||
text-align center
|
||||
.param-line.pt
|
||||
display flex
|
||||
align-items center
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
.el-form
|
||||
.el-form-item__label
|
||||
color var(--text-theme-color)
|
||||
.el-input, .el-slider
|
||||
width 180px
|
||||
.uploader-icon
|
||||
font-size 24px
|
||||
position relative
|
||||
top 3px
|
||||
.no-more-data
|
||||
text-align center
|
||||
padding 30px
|
||||
.generate-btn
|
||||
.iconfont
|
||||
margin-right 5px
|
||||
@@ -1,142 +0,0 @@
|
||||
.page-luma {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background-color: #0e0808;
|
||||
overflow: auto;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, rgba(75,62,53,0.8), rgba(144,50,181,0.3));
|
||||
}
|
||||
.page-luma .prompt-box {
|
||||
display: flex;
|
||||
max-width: 56rem;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
flex-flow: column;
|
||||
}
|
||||
.page-luma .prompt-box .images {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
padding-bottom: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-luma .prompt-box .images .item {
|
||||
position: relative;
|
||||
}
|
||||
.page-luma .prompt-box .images .item .el-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.page-luma .prompt-box .images .item .el-icon {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
color: #545454;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
}
|
||||
.page-luma .prompt-box .images .item .el-icon:hover {
|
||||
color: #888;
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container {
|
||||
width: 100%;
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container {
|
||||
background: linear-gradient(90deg, rgba(75,62,53,0.8), rgba(144,50,181,0.3));
|
||||
border-radius: 28px;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container .prompt-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
line-height: 24px;
|
||||
overflow-wrap: break-word;
|
||||
scrollbar-width: none; /* 隐藏滚动条 */
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container .prompt-input::placeholder {
|
||||
color: rgba(255,255,255,0.6);
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container .prompt-input::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container .upload-icon,
|
||||
.page-luma .prompt-box .prompt-container .input-container .send-icon {
|
||||
color: #e1e1e1;
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container .upload-icon .iconfont,
|
||||
.page-luma .prompt-box .prompt-container .input-container .send-icon .iconfont {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-luma .prompt-box .prompt-container .input-container .upload-icon {
|
||||
position: relative;
|
||||
}
|
||||
.page-luma .video-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
width: 100%;
|
||||
padding: 0 40px;
|
||||
}
|
||||
.page-luma .video-container .h-title {
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
font-size: 36px;
|
||||
text-align: left;
|
||||
}
|
||||
.page-luma .video-container .videos .item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-luma .video-container .videos .item .video-box {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.page-luma .video-container .videos .item .video-box video,
|
||||
.page-luma .video-container .videos .item .video-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-luma .video-container .videos .item .video-name {
|
||||
color: #e1e1e1;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.page-luma .video-container .videos .item .opts {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-luma .video-container .videos .item .opts .btn {
|
||||
margin-right: 10px;
|
||||
background-color: rgba(255,255,255,0.15);
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 3px 15px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.page-luma .video-container .videos .item .opts .btn .iconfont {
|
||||
font-size: 12px;
|
||||
}
|
||||
.page-luma .video-container .videos .item .opts .btn:hover {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
.page-luma {
|
||||
display flex
|
||||
height 100%
|
||||
// background-color #0E0808
|
||||
// background: var(--chat-bg);
|
||||
|
||||
overflow auto
|
||||
//justify-content center
|
||||
flex-flow column
|
||||
align-items center
|
||||
// background: linear-gradient(180deg, rgba(75, 62, 53, 0.8), rgba(144, 50, 181, 0.3));
|
||||
|
||||
|
||||
.prompt-box {
|
||||
display flex
|
||||
max-width 56rem
|
||||
width 100%
|
||||
padding 20px
|
||||
flex-flow column
|
||||
|
||||
.images {
|
||||
display flex
|
||||
flex-flow row
|
||||
padding-bottom 10px
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.item {
|
||||
position relative
|
||||
|
||||
.el-image {
|
||||
width 100px
|
||||
height 100px
|
||||
border-radius 6px
|
||||
margin-right 10px
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
position absolute
|
||||
cursor pointer
|
||||
font-size 20px
|
||||
color #545454
|
||||
right 10px
|
||||
top 0
|
||||
|
||||
&:hover {
|
||||
color #888888
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-swap {
|
||||
margin-right 10px
|
||||
.icon-exchange{
|
||||
color var(--text-theme-color)
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.prompt-container {
|
||||
width: 100%;
|
||||
.input-container {
|
||||
background: var(--chat-bg);
|
||||
// background: linear-gradient(90deg, rgba(75, 62, 53, 0.8), rgba(144, 50, 181, 0.3));
|
||||
border-radius: 28px;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
// box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.prompt-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color var(--text-theme-color);
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
line-height 24px
|
||||
overflow-wrap: break-word;
|
||||
|
||||
scrollbar-width: none; /* 隐藏滚动条 */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-icon, .send-icon {
|
||||
color var( --el-color-primary)
|
||||
.iconfont {
|
||||
font-size 20px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
.upload-icon {
|
||||
position relative
|
||||
}
|
||||
}
|
||||
.params {
|
||||
display flex
|
||||
justify-content right
|
||||
color var(--text-theme-color);
|
||||
font-size 14px
|
||||
padding 10px 30px
|
||||
|
||||
.item-group {
|
||||
margin-left 20px
|
||||
.label {
|
||||
margin-right 5px
|
||||
position relative
|
||||
top 1px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.video-container {
|
||||
display flex
|
||||
flex-flow column
|
||||
width 100%
|
||||
padding 0 40px
|
||||
|
||||
.h-title {
|
||||
color var(--text-theme-color)
|
||||
width 100%
|
||||
// font-size 36px
|
||||
text-align left
|
||||
}
|
||||
|
||||
.list-box {
|
||||
padding 0
|
||||
.item {
|
||||
display flex
|
||||
flex-flow row
|
||||
align-items center
|
||||
min-height 100px
|
||||
padding 10px 15px
|
||||
border-radius 10px
|
||||
cursor pointer
|
||||
margin-bottom 20px
|
||||
background: var(--chat-bg);
|
||||
|
||||
|
||||
|
||||
.left {
|
||||
.container {
|
||||
width 160px
|
||||
position relative
|
||||
|
||||
.video{
|
||||
width 160px
|
||||
border-radius 5px
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width 160px
|
||||
height 90px
|
||||
border-radius 5px
|
||||
}
|
||||
|
||||
.duration {
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
background-color rgba(14,8,8,.7)
|
||||
padding 0 3px
|
||||
font-family 'Input Sans'
|
||||
font-size 14px
|
||||
font-weight 700
|
||||
border-radius .125rem
|
||||
}
|
||||
|
||||
.play {
|
||||
position absolute
|
||||
width: 100%
|
||||
height 100%
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border none
|
||||
border-radius 5px
|
||||
background rgba(100, 100, 100, 0.3)
|
||||
cursor pointer
|
||||
color var(--text-theme-color)
|
||||
|
||||
opacity 0
|
||||
transform: translate(-50%, 0px);
|
||||
transition opacity 0.3s ease 0s
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.play {
|
||||
opacity 1
|
||||
//display block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
width 100%
|
||||
//border 1px solid saddlebrown
|
||||
display flex
|
||||
justify-content center
|
||||
align-items flex-start
|
||||
flex-flow column
|
||||
padding 0 20px
|
||||
|
||||
.prompt,.failed {
|
||||
padding 0
|
||||
font-size 16px
|
||||
max-height 80px
|
||||
line-height 28px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
}
|
||||
.prompt {
|
||||
color var( --text-fb)
|
||||
cursor: text
|
||||
}
|
||||
.failed {
|
||||
color #E4696B
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display flex
|
||||
justify-content right
|
||||
min-width 200px;
|
||||
font-size 14px
|
||||
padding 0
|
||||
|
||||
.tools {
|
||||
display flex
|
||||
justify-content left
|
||||
align-items center
|
||||
flex-flow row
|
||||
height 90px
|
||||
|
||||
.btn-publish {
|
||||
padding 2px 10px
|
||||
|
||||
.text {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background none
|
||||
padding 6px
|
||||
transition background 0.6s ease 0s
|
||||
color #919191
|
||||
|
||||
&:hover {
|
||||
// background #5f5958
|
||||
// color #e1e1e1
|
||||
color:var(--el-color-primary)
|
||||
}
|
||||
|
||||
.downloading {
|
||||
width 16px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top 20px
|
||||
display flex
|
||||
justify-content center
|
||||
}
|
||||
|
||||
//.videos {
|
||||
// .item {
|
||||
// margin-bottom 20px
|
||||
//
|
||||
// .video-box {
|
||||
// width 100%
|
||||
// aspect-ratio: 16/9;
|
||||
// border-radius 10px
|
||||
// video,img {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// object-fit: cover;
|
||||
// border-radius 10px
|
||||
// cursor pointer
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// .video-name {
|
||||
// color #e1e1e1
|
||||
// font-size 16px
|
||||
// white-space nowrap
|
||||
// overflow hidden
|
||||
// text-overflow ellipsis
|
||||
// padding 6px 0
|
||||
// text-align center
|
||||
// }
|
||||
//
|
||||
// .opts {
|
||||
// display flex
|
||||
// justify-content center
|
||||
// .btn {
|
||||
// margin-right 10px
|
||||
// background-color hsla(0,0%,100%,.15)
|
||||
// border none
|
||||
// border-radius 20px
|
||||
// padding 3px 15px
|
||||
// cursor pointer
|
||||
// color var(--text-theme-color)
|
||||
// font-size 14px
|
||||
//
|
||||
// .iconfont {
|
||||
// font-size 11px
|
||||
// position relative
|
||||
// margin-right 5px
|
||||
// top -2px
|
||||
// }
|
||||
//
|
||||
// .el-image {
|
||||
// width 14px
|
||||
// height 14px
|
||||
// margin-right 5px
|
||||
// }
|
||||
//
|
||||
// &:hover {
|
||||
// background-color hsla(0,0%,100%,.2)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-right 10px
|
||||
border none
|
||||
border-radius 5px
|
||||
padding 5px 10px
|
||||
cursor pointer
|
||||
color: var(--theme-text-color-primary)
|
||||
background-color var(--btn-bg)
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -184,21 +184,7 @@ body {
|
||||
.w-100 {
|
||||
width 100%
|
||||
}
|
||||
.mr-1 {
|
||||
margin-right 0.5rem
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right 1rem
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left 0.5rem
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left 1rem
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display flex !important
|
||||
@@ -218,21 +204,3 @@ body {
|
||||
.align-center {
|
||||
align-items center
|
||||
}
|
||||
|
||||
|
||||
|
||||
.p-1 {
|
||||
padding 0.5rem
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding 1rem
|
||||
}
|
||||
|
||||
.m-1 {
|
||||
margin 0.5rem
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin 1rem
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.member {
|
||||
// background-color: #282c34;
|
||||
height 100%
|
||||
|
||||
.title {
|
||||
@@ -13,36 +12,79 @@
|
||||
|
||||
.inner {
|
||||
color var(--text-theme-color)
|
||||
padding 15px 0 15px 15px;
|
||||
padding 15px 0 15px 15px
|
||||
overflow-x hidden
|
||||
overflow-y visible
|
||||
display flex
|
||||
flex-flow row
|
||||
|
||||
.user-profile {
|
||||
padding 10px 20px 20px 20px
|
||||
width 300px
|
||||
background-color var(--chat-bg)
|
||||
color var(--text-theme-color)
|
||||
border-radius 10px
|
||||
//height 100vh
|
||||
|
||||
.el-form-item__label {
|
||||
color var(--text-theme-color)
|
||||
justify-content start
|
||||
.profile-card {
|
||||
max-width 300px
|
||||
border-radius 18px
|
||||
box-shadow 0 4px 8px rgba(0,0,0,0.08)
|
||||
padding 24px 16px
|
||||
background var(--panel-bg)
|
||||
position relative
|
||||
z-index 1
|
||||
margin-bottom 24px
|
||||
}
|
||||
.profile-title {
|
||||
font-size 18px
|
||||
font-weight bold
|
||||
margin-bottom 18px
|
||||
color #2d8cf0
|
||||
letter-spacing 2px
|
||||
text-align center
|
||||
}
|
||||
.profile-btn {
|
||||
width 100%
|
||||
margin-bottom 12px
|
||||
font-size 16px
|
||||
font-weight 500
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
border none
|
||||
border-radius 8px
|
||||
background linear-gradient(90deg, #6dd5ed 0%, #2193b0 100%)
|
||||
color #fff
|
||||
transition all 0.3s
|
||||
i {
|
||||
margin-right 8px
|
||||
font-size 20px
|
||||
}
|
||||
|
||||
.user-opt {
|
||||
.el-col {
|
||||
padding 10px
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
box-shadow 0 2px 12px #2193b0aa
|
||||
transform translateY(-2px) scale(1.03)
|
||||
background linear-gradient(90deg, #2193b0 0%, #6dd5ed 100%)
|
||||
}
|
||||
}
|
||||
|
||||
.profile-btn.email {
|
||||
background linear-gradient(90deg, #f7971e 0%, #ffd200 100%)
|
||||
}
|
||||
.profile-btn.mobile {
|
||||
background linear-gradient(90deg, #43cea2 0%, #185a9d 100%)
|
||||
}
|
||||
.profile-btn.third {
|
||||
background linear-gradient(90deg, #ff512f 0%, #dd2476 100%)
|
||||
}
|
||||
.profile-btn.password {
|
||||
background linear-gradient(90deg, #1d4350 0%, #a43931 100%)
|
||||
}
|
||||
.profile-btn.redeem {
|
||||
background linear-gradient(90deg, #00c6ff 0%, #0072ff 100%)
|
||||
}
|
||||
.profile-bg {
|
||||
position absolute
|
||||
left 0
|
||||
top 0
|
||||
width 100%
|
||||
height 100%
|
||||
z-index 0
|
||||
background url('data:image/svg+xml;utf8,<svg width="100%25" height="100%25" viewBox="0 0 400 200" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="100" cy="100" r="80" fill="%23e0eaff"/><circle cx="300" cy="60" r="40" fill="%23f0f7ff"/><circle cx="320" cy="180" r="30" fill="%23e0eaff"/></svg>') no-repeat center/cover
|
||||
opacity 0.08
|
||||
pointer-events none
|
||||
}
|
||||
|
||||
.product-box {
|
||||
padding 0 20px
|
||||
|
||||
@@ -96,4 +96,7 @@
|
||||
|
||||
// el-dialog 阴影
|
||||
--el-box-shadow: 0 0 15px rgba(107, 80, 225, 0.8);
|
||||
|
||||
// 面板背景
|
||||
--panel-bg: linear-gradient(135deg, #252d58 0%, #1f243f 100%);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--text-fb:#000;
|
||||
--text-color: #5b62ce; // 主要的文本颜色
|
||||
--normal-color: rgba(43, 54, 116, 1); // 普通颜色
|
||||
--theme-textcolor-normal:#5b62ce;;
|
||||
p, h1, h2, h3, h4, h5, h6, article {
|
||||
font-family: $font-regular;
|
||||
}
|
||||
@@ -56,6 +57,8 @@
|
||||
// 引用快样式
|
||||
--quote-bg-color: #e0dfff;
|
||||
--quote-text-color: #333;
|
||||
// 面板背景
|
||||
--panel-bg: linear-gradient(135deg, #f5eafe 0%, #e9e6fc 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
567
web/src/assets/css/video.styl
Normal file
567
web/src/assets/css/video.styl
Normal file
@@ -0,0 +1,567 @@
|
||||
// 视频生成页面统一样式
|
||||
.page-video {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--chat-bg);
|
||||
|
||||
// Element Plus 样式覆盖
|
||||
:deep(.el-form-item__label) {
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
|
||||
:deep(.el-textarea) {
|
||||
--el-input-focus-border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
background: transparent;
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
// 左侧参数面板
|
||||
.params-panel {
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
margin: 10px;
|
||||
padding: 0 15px 20px 15px;
|
||||
border-radius: 10px;
|
||||
color: var(--text-theme-color);
|
||||
font-size: 14px;
|
||||
overflow: auto;
|
||||
background: var(--card-bg);
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: var(--theme-textcolor-normal);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// 标签页样式
|
||||
.video-type-tabs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: var(--theme-textcolor-normal);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
color: var(--text-theme-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__active-bar) {
|
||||
background-color: var(--theme-textcolor-normal);
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
// 参数行
|
||||
.param-line {
|
||||
padding: 5px 0;
|
||||
|
||||
&.pt {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单项样式
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
|
||||
.el-input, .el-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%;
|
||||
--el-select-input-focus-border-color: var(--el-color-primary);
|
||||
--el-input-focus-border-color: var(--el-color-primary);
|
||||
|
||||
.el-input__wrapper {
|
||||
background: var(--chat-bg);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 网格内容项
|
||||
.grid-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--chat-bg);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--theme-border-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid var(--theme-border-hover);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&.proportion {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.texts {
|
||||
margin-left: 5px;
|
||||
margin-top: 2px;
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 运镜控制
|
||||
.camera-control {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: var(--card-bg);
|
||||
|
||||
:deep(.el-form-item:last-child) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成按钮
|
||||
.generate-btn {
|
||||
.iconfont {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// 项目组样式
|
||||
.item-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.label {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 图片模式切换样式
|
||||
.image-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.label {
|
||||
margin-right: 10px;
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 过渡动画
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// KeLing 参数面板特有样式
|
||||
.params-container {
|
||||
// 任务类型标签页
|
||||
.task-type-tabs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传样式
|
||||
.img-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.img-uploader {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.removeimg {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
color: #f56c6c;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.btn-swap {
|
||||
.icon-exchange {
|
||||
color: var(--text-theme-color);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
.submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 算力信息
|
||||
.text-info {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
|
||||
.el-tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧主要内容区域
|
||||
.main-content {
|
||||
padding: 1.5rem;
|
||||
flex: 1;
|
||||
background: var(--chat-bg);
|
||||
color: var(--text-theme-color);
|
||||
overflow-x: hidden;
|
||||
|
||||
// 作品标题栏
|
||||
.works-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.h-title {
|
||||
color: var(--text-theme-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// .filter-buttons {
|
||||
// .el-button-group {
|
||||
// .el-button {
|
||||
// --el-button-bg-color: var(--card-bg);
|
||||
// --el-button-border-color: var(--chat-bg);
|
||||
// --el-button-text-color: var(--text-theme-color);
|
||||
// --el-button-hover-bg-color: var(--theme-border-hover);
|
||||
// --el-button-hover-border-color: var(--theme-border-hover);
|
||||
// --el-button-active-bg-color: var(--el-color-primary);
|
||||
// --el-button-active-border-color: var(--el-color-primary);
|
||||
|
||||
// &.is-type-primary {
|
||||
// --el-button-bg-color: var(--el-color-primary);
|
||||
// --el-button-border-color: var(--el-color-primary);
|
||||
// --el-button-text-color: #ffffff;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// 任务列表
|
||||
.video-list {
|
||||
.list-box {
|
||||
padding: 0;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
background: var(--card-bg);
|
||||
|
||||
.left {
|
||||
.container {
|
||||
width: 160px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.video {
|
||||
width: 160px;
|
||||
height: 120px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.duration {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(14, 8, 8, 0.7);
|
||||
padding: 0 3px;
|
||||
font-family: 'Input Sans';
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.play {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: rgba(100, 100, 100, 0.3);
|
||||
cursor: pointer;
|
||||
color: var(--text-theme-color);
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 0px);
|
||||
transition: opacity 0.3s ease 0s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.play {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-flow: column;
|
||||
padding: 0 20px;
|
||||
|
||||
.prompt, .failed {
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
max-height: 80px;
|
||||
line-height: 28px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--text-fb);
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: #E4696B;
|
||||
}
|
||||
|
||||
.pb-2 {
|
||||
padding-bottom: 8px;
|
||||
|
||||
.el-tag {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
min-width: 200px;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
|
||||
.tools {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
flex-flow: row;
|
||||
height: 90px;
|
||||
|
||||
.btn-publish {
|
||||
padding: 2px 10px;
|
||||
|
||||
.text {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
padding: 6px;
|
||||
transition: background 0.6s ease 0s;
|
||||
color: #919191;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.downloading {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通用按钮样式
|
||||
.btn {
|
||||
margin-right: 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
color: var(--theme-text-color-primary);
|
||||
background-color: var(--btn-bg);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// 无数据样式
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-video {
|
||||
flex-direction: column;
|
||||
|
||||
.params-panel {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
|
||||
.video-list .list-box .item {
|
||||
.left .container {
|
||||
width: 120px;
|
||||
|
||||
.video, .el-image {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.right {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1740279975534') format('woff2'),
|
||||
url('iconfont.woff?t=1740279975534') format('woff'),
|
||||
url('iconfont.ttf?t=1740279975534') format('truetype');
|
||||
src: url('iconfont.woff2?t=1752831319382') format('woff2'),
|
||||
url('iconfont.woff?t=1752831319382') format('woff'),
|
||||
url('iconfont.ttf?t=1752831319382') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,150 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-jimeng2:before {
|
||||
content: "\eabc";
|
||||
}
|
||||
|
||||
.icon-jimeng:before {
|
||||
content: "\eabb";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e63f";
|
||||
}
|
||||
|
||||
.icon-empty-box:before {
|
||||
content: "\e638";
|
||||
}
|
||||
|
||||
.icon-check2:before {
|
||||
content: "\e7e2";
|
||||
}
|
||||
|
||||
.icon-creator:before {
|
||||
content: "\e6a1";
|
||||
}
|
||||
|
||||
.icon-withdraw:before {
|
||||
content: "\e689";
|
||||
}
|
||||
|
||||
.icon-withdraw-log:before {
|
||||
content: "\e635";
|
||||
}
|
||||
|
||||
.icon-money:before {
|
||||
content: "\e831";
|
||||
}
|
||||
|
||||
.icon-doller:before {
|
||||
content: "\e633";
|
||||
}
|
||||
|
||||
.icon-wallet:before {
|
||||
content: "\e64d";
|
||||
}
|
||||
|
||||
.icon-check:before {
|
||||
content: "\e810";
|
||||
}
|
||||
|
||||
.icon-refuse:before {
|
||||
content: "\e629";
|
||||
}
|
||||
|
||||
.icon-Reject:before {
|
||||
content: "\e70d";
|
||||
}
|
||||
|
||||
.icon-clock:before {
|
||||
content: "\e65d";
|
||||
}
|
||||
|
||||
.icon-eye-close:before {
|
||||
content: "\e7aa";
|
||||
}
|
||||
|
||||
.icon-eye-open:before {
|
||||
content: "\e7ab";
|
||||
}
|
||||
|
||||
.icon-list:before {
|
||||
content: "\e650";
|
||||
}
|
||||
|
||||
.icon-categroy:before {
|
||||
content: "\e620";
|
||||
}
|
||||
|
||||
.icon-zhankai:before {
|
||||
content: "\e632";
|
||||
}
|
||||
|
||||
.icon-wechat-mini:before {
|
||||
content: "\e63d";
|
||||
}
|
||||
|
||||
.icon-niutou:before {
|
||||
content: "\e64c";
|
||||
}
|
||||
|
||||
.icon-qiniu:before {
|
||||
content: "\e62c";
|
||||
}
|
||||
|
||||
.icon-storage:before {
|
||||
content: "\e69a";
|
||||
}
|
||||
|
||||
.icon-localstorage:before {
|
||||
content: "\ea8d";
|
||||
}
|
||||
|
||||
.icon-minio:before {
|
||||
content: "\e855";
|
||||
}
|
||||
|
||||
.icon-aliyun:before {
|
||||
content: "\e672";
|
||||
}
|
||||
|
||||
.icon-sms:before {
|
||||
content: "\e82c";
|
||||
}
|
||||
|
||||
.icon-duanxin:before {
|
||||
content: "\e65c";
|
||||
}
|
||||
|
||||
.icon-yanzm:before {
|
||||
content: "\e625";
|
||||
}
|
||||
|
||||
.icon-yaoqm:before {
|
||||
content: "\e66e";
|
||||
}
|
||||
|
||||
.icon-epay:before {
|
||||
content: "\e628";
|
||||
}
|
||||
|
||||
.icon-coze:before {
|
||||
content: "\e61b";
|
||||
}
|
||||
|
||||
.icon-token:before {
|
||||
content: "\e68e";
|
||||
}
|
||||
|
||||
.icon-reset:before {
|
||||
content: "\e649";
|
||||
}
|
||||
|
||||
.icon-stats:before {
|
||||
content: "\e878";
|
||||
}
|
||||
|
||||
.icon-keling:before {
|
||||
content: "\eab7";
|
||||
}
|
||||
@@ -289,7 +433,7 @@
|
||||
content: "\e6c4";
|
||||
}
|
||||
|
||||
.icon-mp1:before {
|
||||
.icon-mp4:before {
|
||||
content: "\e647";
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,258 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42693930",
|
||||
"name": "即梦AI-02",
|
||||
"font_class": "jimeng2",
|
||||
"unicode": "eabc",
|
||||
"unicode_decimal": 60092
|
||||
},
|
||||
{
|
||||
"icon_id": "42693927",
|
||||
"name": "即梦AI-01",
|
||||
"font_class": "jimeng",
|
||||
"unicode": "eabb",
|
||||
"unicode_decimal": 60091
|
||||
},
|
||||
{
|
||||
"icon_id": "1283",
|
||||
"name": "视频",
|
||||
"font_class": "video",
|
||||
"unicode": "e63f",
|
||||
"unicode_decimal": 58943
|
||||
},
|
||||
{
|
||||
"icon_id": "35224131",
|
||||
"name": "empty-box",
|
||||
"font_class": "empty-box",
|
||||
"unicode": "e638",
|
||||
"unicode_decimal": 58936
|
||||
},
|
||||
{
|
||||
"icon_id": "9143175",
|
||||
"name": "审核",
|
||||
"font_class": "check2",
|
||||
"unicode": "e7e2",
|
||||
"unicode_decimal": 59362
|
||||
},
|
||||
{
|
||||
"icon_id": "15450788",
|
||||
"name": "创作者中心",
|
||||
"font_class": "creator",
|
||||
"unicode": "e6a1",
|
||||
"unicode_decimal": 59041
|
||||
},
|
||||
{
|
||||
"icon_id": "1134341",
|
||||
"name": "提现",
|
||||
"font_class": "withdraw",
|
||||
"unicode": "e689",
|
||||
"unicode_decimal": 59017
|
||||
},
|
||||
{
|
||||
"icon_id": "10887127",
|
||||
"name": "提现记录",
|
||||
"font_class": "withdraw-log",
|
||||
"unicode": "e635",
|
||||
"unicode_decimal": 58933
|
||||
},
|
||||
{
|
||||
"icon_id": "34452904",
|
||||
"name": "money-rmb",
|
||||
"font_class": "money",
|
||||
"unicode": "e831",
|
||||
"unicode_decimal": 59441
|
||||
},
|
||||
{
|
||||
"icon_id": "34467697",
|
||||
"name": "doller",
|
||||
"font_class": "doller",
|
||||
"unicode": "e633",
|
||||
"unicode_decimal": 58931
|
||||
},
|
||||
{
|
||||
"icon_id": "9512709",
|
||||
"name": "钱包¥",
|
||||
"font_class": "wallet",
|
||||
"unicode": "e64d",
|
||||
"unicode_decimal": 58957
|
||||
},
|
||||
{
|
||||
"icon_id": "8365142",
|
||||
"name": "check",
|
||||
"font_class": "check",
|
||||
"unicode": "e810",
|
||||
"unicode_decimal": 59408
|
||||
},
|
||||
{
|
||||
"icon_id": "10213506",
|
||||
"name": "refuse",
|
||||
"font_class": "refuse",
|
||||
"unicode": "e629",
|
||||
"unicode_decimal": 58921
|
||||
},
|
||||
{
|
||||
"icon_id": "19393806",
|
||||
"name": "Reject",
|
||||
"font_class": "Reject",
|
||||
"unicode": "e70d",
|
||||
"unicode_decimal": 59149
|
||||
},
|
||||
{
|
||||
"icon_id": "248916",
|
||||
"name": "clock",
|
||||
"font_class": "clock",
|
||||
"unicode": "e65d",
|
||||
"unicode_decimal": 58973
|
||||
},
|
||||
{
|
||||
"icon_id": "6151096",
|
||||
"name": "eye-close",
|
||||
"font_class": "eye-close",
|
||||
"unicode": "e7aa",
|
||||
"unicode_decimal": 59306
|
||||
},
|
||||
{
|
||||
"icon_id": "6151097",
|
||||
"name": "eye-open",
|
||||
"font_class": "eye-open",
|
||||
"unicode": "e7ab",
|
||||
"unicode_decimal": 59307
|
||||
},
|
||||
{
|
||||
"icon_id": "6145570",
|
||||
"name": "list",
|
||||
"font_class": "list",
|
||||
"unicode": "e650",
|
||||
"unicode_decimal": 58960
|
||||
},
|
||||
{
|
||||
"icon_id": "13127646",
|
||||
"name": "categroy",
|
||||
"font_class": "categroy",
|
||||
"unicode": "e620",
|
||||
"unicode_decimal": 58912
|
||||
},
|
||||
{
|
||||
"icon_id": "1613505",
|
||||
"name": "展开",
|
||||
"font_class": "zhankai",
|
||||
"unicode": "e632",
|
||||
"unicode_decimal": 58930
|
||||
},
|
||||
{
|
||||
"icon_id": "10905663",
|
||||
"name": "微信小程序",
|
||||
"font_class": "wechat-mini",
|
||||
"unicode": "e63d",
|
||||
"unicode_decimal": 58941
|
||||
},
|
||||
{
|
||||
"icon_id": "21530643",
|
||||
"name": "牛头",
|
||||
"font_class": "niutou",
|
||||
"unicode": "e64c",
|
||||
"unicode_decimal": 58956
|
||||
},
|
||||
{
|
||||
"icon_id": "24877229",
|
||||
"name": "七牛云",
|
||||
"font_class": "qiniu",
|
||||
"unicode": "e62c",
|
||||
"unicode_decimal": 58924
|
||||
},
|
||||
{
|
||||
"icon_id": "3717493",
|
||||
"name": "存储服务",
|
||||
"font_class": "storage",
|
||||
"unicode": "e69a",
|
||||
"unicode_decimal": 59034
|
||||
},
|
||||
{
|
||||
"icon_id": "7133059",
|
||||
"name": "本地存储",
|
||||
"font_class": "localstorage",
|
||||
"unicode": "ea8d",
|
||||
"unicode_decimal": 60045
|
||||
},
|
||||
{
|
||||
"icon_id": "9360420",
|
||||
"name": "minio",
|
||||
"font_class": "minio",
|
||||
"unicode": "e855",
|
||||
"unicode_decimal": 59477
|
||||
},
|
||||
{
|
||||
"icon_id": "21053628",
|
||||
"name": "阿里云",
|
||||
"font_class": "aliyun",
|
||||
"unicode": "e672",
|
||||
"unicode_decimal": 58994
|
||||
},
|
||||
{
|
||||
"icon_id": "30046100",
|
||||
"name": "comment-sms",
|
||||
"font_class": "sms",
|
||||
"unicode": "e82c",
|
||||
"unicode_decimal": 59436
|
||||
},
|
||||
{
|
||||
"icon_id": "4893414",
|
||||
"name": "短信",
|
||||
"font_class": "duanxin",
|
||||
"unicode": "e65c",
|
||||
"unicode_decimal": 58972
|
||||
},
|
||||
{
|
||||
"icon_id": "553324",
|
||||
"name": "验证码",
|
||||
"font_class": "yanzm",
|
||||
"unicode": "e625",
|
||||
"unicode_decimal": 58917
|
||||
},
|
||||
{
|
||||
"icon_id": "1264836",
|
||||
"name": "邀请码",
|
||||
"font_class": "yaoqm",
|
||||
"unicode": "e66e",
|
||||
"unicode_decimal": 58990
|
||||
},
|
||||
{
|
||||
"icon_id": "24827618",
|
||||
"name": "网易支付",
|
||||
"font_class": "epay",
|
||||
"unicode": "e628",
|
||||
"unicode_decimal": 58920
|
||||
},
|
||||
{
|
||||
"icon_id": "43863501",
|
||||
"name": "Coze",
|
||||
"font_class": "coze",
|
||||
"unicode": "e61b",
|
||||
"unicode_decimal": 58907
|
||||
},
|
||||
{
|
||||
"icon_id": "11551884",
|
||||
"name": "token",
|
||||
"font_class": "token",
|
||||
"unicode": "e68e",
|
||||
"unicode_decimal": 59022
|
||||
},
|
||||
{
|
||||
"icon_id": "38795534",
|
||||
"name": "reset",
|
||||
"font_class": "reset",
|
||||
"unicode": "e649",
|
||||
"unicode_decimal": 58953
|
||||
},
|
||||
{
|
||||
"icon_id": "5838820",
|
||||
"name": "统计",
|
||||
"font_class": "stats",
|
||||
"unicode": "e878",
|
||||
"unicode_decimal": 59512
|
||||
},
|
||||
{
|
||||
"icon_id": "42692844",
|
||||
"name": "可灵大模型",
|
||||
@@ -491,7 +743,7 @@
|
||||
{
|
||||
"icon_id": "12600802",
|
||||
"name": "mp4",
|
||||
"font_class": "mp1",
|
||||
"font_class": "mp4",
|
||||
"unicode": "e647",
|
||||
"unicode_decimal": 58951
|
||||
},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -124,7 +124,7 @@ import hl from 'highlight.js'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { ref } from 'vue'
|
||||
import { nextTick, onMounted, reactive, ref, watchEffect } from 'vue'
|
||||
import Thinking from './Thinking.vue'
|
||||
// eslint-disable-next-line no-undef,no-unused-vars
|
||||
const props = defineProps({
|
||||
@@ -155,6 +155,9 @@ const isPlaying = ref(false)
|
||||
const playIcon = ref('/images/voice.gif')
|
||||
const store = useSharedStore()
|
||||
|
||||
// 添加代码块展开/收起状态管理
|
||||
const codeBlockStates = reactive({})
|
||||
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
html: true,
|
||||
@@ -162,24 +165,29 @@ const md = new MarkdownIt({
|
||||
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(
|
||||
// 显示复制代码按钮和展开/收起按钮
|
||||
const copyBtn = `<div class="flex">
|
||||
<span class="text-[12px] mr-2 text-[#00e0e0] cursor-pointer expand-btn" data-code-id="${codeIndex}">展开</span>
|
||||
<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
</div><textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
'</textarea>'
|
||||
)}</textarea>`
|
||||
let langHtml = ''
|
||||
let preCode = ''
|
||||
// 处理代码高亮
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(str, { language: lang }).value
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
|
||||
langHtml = `<span class="lang-name">${lang}</span>`
|
||||
preCode = hl.highlight(str, { language: lang }).value
|
||||
} else {
|
||||
preCode = md.utils.escapeHtml(str)
|
||||
}
|
||||
|
||||
// 处理代码高亮
|
||||
const preCode = md.utils.escapeHtml(str)
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
|
||||
// 将代码包裹在 pre 中,添加收起状态的类
|
||||
return `<pre class="code-container flex flex-col code-collapsed" data-code-id="${codeIndex}">
|
||||
<div class="flex justify-between bg-[#50505a] w-full rounded-tl-[10px] rounded-tr-[10px] px-3 py-1">${langHtml}${copyBtn}</div>
|
||||
<code class="language-${lang} hljs">${preCode}</code>
|
||||
<span class="copy-code-btn absolute right-3 bottom-3" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span></pre>`
|
||||
},
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
@@ -226,6 +234,81 @@ const stopSynthesis = () => {
|
||||
const reGenerate = (messageId) => {
|
||||
emits('regen', messageId)
|
||||
}
|
||||
|
||||
// 添加代码块展开/收起功能
|
||||
const toggleCodeBlock = (codeId) => {
|
||||
const codeContainer = document.querySelector(`pre[data-code-id="${codeId}"]`)
|
||||
const expandBtn = document.querySelector(`.expand-btn[data-code-id="${codeId}"]`)
|
||||
|
||||
if (codeContainer && expandBtn) {
|
||||
if (codeContainer.classList.contains('code-collapsed')) {
|
||||
codeContainer.classList.remove('code-collapsed')
|
||||
codeContainer.classList.add('code-expanded')
|
||||
expandBtn.textContent = '收起'
|
||||
} else {
|
||||
codeContainer.classList.remove('code-expanded')
|
||||
codeContainer.classList.add('code-collapsed')
|
||||
expandBtn.textContent = '展开'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加事件监听
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupCodeBlockEvents()
|
||||
})
|
||||
})
|
||||
|
||||
// 监听内容变化,重新绑定事件
|
||||
watchEffect(() => {
|
||||
if (props.data.content.text) {
|
||||
nextTick(() => {
|
||||
setupCodeBlockEvents()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const setupCodeBlockEvents = () => {
|
||||
// 移除旧的事件监听器
|
||||
const oldBtns = document.querySelectorAll('.expand-btn')
|
||||
oldBtns.forEach((btn) => {
|
||||
btn.removeEventListener('click', handleExpandClick)
|
||||
})
|
||||
|
||||
// 为展开按钮添加点击事件
|
||||
const expandBtns = document.querySelectorAll('.expand-btn')
|
||||
expandBtns.forEach((btn) => {
|
||||
btn.addEventListener('click', handleExpandClick)
|
||||
|
||||
// 检查对应的代码块是否需要展开功能
|
||||
const codeId = btn.getAttribute('data-code-id')
|
||||
const codeContainer = document.querySelector(`pre[data-code-id="${codeId}"]`)
|
||||
const codeElement = codeContainer?.querySelector('.hljs')
|
||||
|
||||
if (codeElement) {
|
||||
// 临时移除高度限制来获取真实高度
|
||||
const originalMaxHeight = codeElement.style.maxHeight
|
||||
codeElement.style.maxHeight = 'none'
|
||||
const realHeight = codeElement.scrollHeight
|
||||
codeElement.style.maxHeight = originalMaxHeight
|
||||
|
||||
// 如果代码块高度小于等于200px,隐藏展开按钮
|
||||
if (realHeight <= 200) {
|
||||
btn.style.display = 'none'
|
||||
// 移除收起状态的类,让短代码块完全展示
|
||||
codeContainer.classList.remove('code-collapsed')
|
||||
} else {
|
||||
btn.style.display = 'inline'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleExpandClick = (e) => {
|
||||
const codeId = e.target.getAttribute('data-code-id')
|
||||
toggleCodeBlock(codeId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -266,8 +349,9 @@ const reGenerate = (messageId) => {
|
||||
}
|
||||
|
||||
.code-container {
|
||||
background-color #2b2b2b
|
||||
border-radius 10px
|
||||
position relative
|
||||
display flex
|
||||
|
||||
.hljs {
|
||||
border-radius 10px
|
||||
@@ -275,9 +359,6 @@ const reGenerate = (messageId) => {
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
right 10px
|
||||
top 10px
|
||||
cursor pointer
|
||||
font-size 12px
|
||||
color #c1c1c1
|
||||
@@ -289,16 +370,50 @@ const reGenerate = (messageId) => {
|
||||
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
position absolute;
|
||||
right 10px
|
||||
bottom 20px
|
||||
padding 2px 6px 4px 6px
|
||||
background-color #444444
|
||||
border-radius 10px
|
||||
color #00e0e0
|
||||
// 添加代码块展开/收起样式
|
||||
.code-collapsed {
|
||||
.hljs {
|
||||
max-height 200px
|
||||
overflow hidden
|
||||
position relative
|
||||
transition max-height 0.3s ease
|
||||
|
||||
&::after {
|
||||
content ''
|
||||
position absolute
|
||||
bottom 0
|
||||
left 0
|
||||
right 0
|
||||
height 30px
|
||||
background linear-gradient(transparent, #2b2b2b)
|
||||
pointer-events none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-expanded {
|
||||
.hljs {
|
||||
max-height none
|
||||
overflow auto
|
||||
transition max-height 0.3s ease
|
||||
|
||||
&::after {
|
||||
display none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
transition color 0.2s ease
|
||||
|
||||
&:hover {
|
||||
color #20a0ff !important
|
||||
}
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
color #00e0e0
|
||||
}
|
||||
|
||||
// 设置表格边框
|
||||
|
||||
|
||||
330
web/src/components/ImageUpload.vue
Normal file
330
web/src/components/ImageUpload.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<!-- 单图模式 -->
|
||||
<template v-if="props.maxCount === 1">
|
||||
<div class="single-upload">
|
||||
<div v-if="imageList.length === 0" class="upload-btn">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="false"
|
||||
accept="image/*"
|
||||
class="uploader"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon :size="20"><UploadFilled /></el-icon>
|
||||
<span>上传图片</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div v-else class="upload-item single-image-item">
|
||||
<el-image :src="imageList[0]" fit="cover" class="upload-image" />
|
||||
<div class="upload-overlay" style="opacity: 1">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(0)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 多图模式 -->
|
||||
<template v-else>
|
||||
<div class="upload-list" v-if="imageList.length > 0">
|
||||
<div v-for="(image, index) in imageList" :key="index" class="upload-item">
|
||||
<el-image :src="image" fit="cover" class="upload-image" />
|
||||
<div class="upload-overlay">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(index)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
<div v-if="!multiple || imageList.length < maxCount" class="upload-btn">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="multiple"
|
||||
accept="image/*"
|
||||
class="uploader"
|
||||
:limit="maxCount"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon :size="20"><UploadFilled /></el-icon>
|
||||
<span>上传图片</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 初始上传区域 -->
|
||||
<div v-else class="upload-area">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="multiple"
|
||||
accept="image/*"
|
||||
class="uploader"
|
||||
:limit="maxCount"
|
||||
>
|
||||
<el-icon :size="40" class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">拖拽图片到此处,或 <em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-center">
|
||||
支持 JPG、PNG 格式,最多上传 {{ maxCount }} 张,单张最大 5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<el-progress
|
||||
v-if="uploading"
|
||||
:percentage="uploadProgress"
|
||||
:stroke-width="4"
|
||||
class="upload-progress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
import { Delete, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'upload-success'])
|
||||
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
|
||||
// 图片列表
|
||||
const imageList = computed({
|
||||
get() {
|
||||
if (props.multiple || props.maxCount > 1) {
|
||||
return Array.isArray(props.modelValue) ? props.modelValue : []
|
||||
} else {
|
||||
return props.modelValue ? [props.modelValue] : []
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
if (props.multiple || props.maxCount > 1) {
|
||||
emit('update:modelValue', value)
|
||||
} else {
|
||||
emit('update:modelValue', value[0] || '')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const uploadCount = ref(1)
|
||||
// 处理上传
|
||||
const handleUpload = async (uploadFile) => {
|
||||
const file = uploadFile.file
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小 (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
ElMessage.error('图片大小不能超过 5MB')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (uploadCount.value > props.maxCount) {
|
||||
ElMessage.error(`最多只能上传 ${props.maxCount} 张图片`)
|
||||
return
|
||||
}
|
||||
uploadCount.value++
|
||||
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// 模拟上传进度
|
||||
const progressTimer = setInterval(() => {
|
||||
if (uploadProgress.value < 90) {
|
||||
uploadProgress.value += 10
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const response = await httpPost('/api/upload', formData)
|
||||
|
||||
clearInterval(progressTimer)
|
||||
uploadProgress.value = 100
|
||||
|
||||
const imageUrl = replaceImg(response.data.url)
|
||||
|
||||
// 更新图片列表
|
||||
if (props.multiple || props.maxCount > 1) {
|
||||
const newList = [...imageList.value, imageUrl]
|
||||
imageList.value = newList
|
||||
} else {
|
||||
imageList.value = [imageUrl]
|
||||
}
|
||||
|
||||
emit('upload-success', imageUrl)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('上传失败: ' + (error.message || '网络错误'))
|
||||
} finally {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 移除图片
|
||||
const removeImage = (index) => {
|
||||
const newList = [...imageList.value]
|
||||
newList.splice(index, 1)
|
||||
imageList.value = newList
|
||||
uploadCount.value--
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.single-upload {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.single-image-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.remove-btn {
|
||||
background: rgba(245, 108, 108, 0.8);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .upload-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
.uploader {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
.uploader {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -161,6 +161,24 @@ const items = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'jimeng',
|
||||
index: '/admin/jimeng',
|
||||
title: '即梦AI',
|
||||
subs: [
|
||||
{
|
||||
icon: 'list',
|
||||
index: '/admin/jimeng/jobs',
|
||||
title: '任务列表',
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'config',
|
||||
index: '/admin/jimeng/config',
|
||||
title: '即梦设置',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'role',
|
||||
|
||||
@@ -104,16 +104,16 @@ const routes = [
|
||||
component: () => import('@/views/Song.vue'),
|
||||
},
|
||||
{
|
||||
name: 'luma',
|
||||
path: '/luma',
|
||||
meta: { title: 'Luma视频创作' },
|
||||
component: () => import('@/views/Luma.vue'),
|
||||
name: 'video',
|
||||
path: '/video',
|
||||
meta: { title: '视频创作中心' },
|
||||
component: () => import('@/views/Video.vue'),
|
||||
},
|
||||
{
|
||||
name: 'keling',
|
||||
path: '/keling',
|
||||
meta: { title: 'KeLing视频创作' },
|
||||
component: () => import('@/views/KeLing.vue'),
|
||||
name: 'jimeng',
|
||||
path: '/jimeng',
|
||||
meta: { title: '即梦AI' },
|
||||
component: () => import('@/views/Jimeng.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -258,6 +258,18 @@ const routes = [
|
||||
meta: { title: '音视频管理' },
|
||||
component: () => import('@/views/admin/records/Medias.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/jimeng/jobs',
|
||||
name: 'admin-jimeng-jobs',
|
||||
meta: { title: '即梦AI任务' },
|
||||
component: () => import('@/views/admin/jimeng/JimengJobs.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/jimeng/config',
|
||||
name: 'admin-jimeng-config',
|
||||
meta: { title: '即梦设置' },
|
||||
component: () => import('@/views/admin/jimeng/JimengSetting.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/powerLog',
|
||||
name: 'admin-power-log',
|
||||
@@ -358,4 +370,4 @@ router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
export { router, prevRoute }
|
||||
export { prevRoute, router }
|
||||
|
||||
642
web/src/store/jimeng.js
Normal file
642
web/src/store/jimeng.js
Normal file
@@ -0,0 +1,642 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr } from '@/utils/libs'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
|
||||
export const useJimengStore = defineStore('jimeng', () => {
|
||||
// 当前激活的功能分类和具体功能
|
||||
const activeCategory = ref('image_generation')
|
||||
const activeFunction = ref('text_to_image')
|
||||
const useImageInput = ref(false)
|
||||
|
||||
// 新增:全局提示词
|
||||
const currentPrompt = ref('')
|
||||
|
||||
// 共同状态
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskFilter = ref('all')
|
||||
const currentList = ref([])
|
||||
const isOver = ref(false)
|
||||
|
||||
// 用户信息
|
||||
const isLogin = ref(false)
|
||||
const userPower = ref(100)
|
||||
|
||||
// 视频预览
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
|
||||
// 登录弹窗
|
||||
const shareStore = useSharedStore()
|
||||
|
||||
// 功能分类配置
|
||||
const categories = [
|
||||
{ key: 'image_generation', name: '图片生成' },
|
||||
{ key: 'image_editing', name: 'AI修图' },
|
||||
{ key: 'image_effects', name: '图像特效' },
|
||||
{ key: 'video_generation', name: '视频生成' },
|
||||
]
|
||||
|
||||
// 新增:动态获取算力消耗配置
|
||||
const powerConfig = reactive({})
|
||||
|
||||
// 功能配置
|
||||
const functions = reactive([
|
||||
{
|
||||
key: 'text_to_image',
|
||||
name: '文生图',
|
||||
category: 'image_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: false,
|
||||
power: 20,
|
||||
},
|
||||
{
|
||||
key: 'image_to_image',
|
||||
name: '图生图',
|
||||
category: 'image_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: true,
|
||||
power: 30,
|
||||
},
|
||||
{
|
||||
key: 'image_edit',
|
||||
name: '图像编辑',
|
||||
category: 'image_editing',
|
||||
needsPrompt: true,
|
||||
needsImage: true,
|
||||
multiple: true,
|
||||
power: 25,
|
||||
},
|
||||
{
|
||||
key: 'image_effects',
|
||||
name: '图像特效',
|
||||
category: 'image_effects',
|
||||
needsPrompt: false,
|
||||
needsImage: true,
|
||||
power: 15,
|
||||
},
|
||||
{
|
||||
key: 'text_to_video',
|
||||
name: '文生视频',
|
||||
category: 'video_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: false,
|
||||
power: 100,
|
||||
},
|
||||
{
|
||||
key: 'image_to_video',
|
||||
name: '图生视频',
|
||||
category: 'video_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: true,
|
||||
multiple: true,
|
||||
power: 120,
|
||||
},
|
||||
])
|
||||
|
||||
// 动态设置算力消耗
|
||||
const setFunctionPowers = (config) => {
|
||||
functions.forEach((f) => {
|
||||
if (config[f.key] !== undefined) {
|
||||
f.power = config[f.key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 各功能的参数
|
||||
const textToImageParams = reactive({
|
||||
size: '1328x1328',
|
||||
scale: 2.5,
|
||||
seed: -1,
|
||||
use_pre_llm: true,
|
||||
})
|
||||
|
||||
const imageToImageParams = reactive({
|
||||
image_input: '',
|
||||
size: '1328x1328',
|
||||
gpen: 0.4,
|
||||
skin: 0.3,
|
||||
skin_unifi: 0,
|
||||
gen_mode: 'creative',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
const imageEditParams = reactive({
|
||||
image_urls: '',
|
||||
scale: 0.5,
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
const imageEffectsParams = reactive({
|
||||
image_input1: '',
|
||||
template_id: '',
|
||||
size: '1328x1328',
|
||||
})
|
||||
|
||||
const textToVideoParams = reactive({
|
||||
aspect_ratio: '16:9',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
const imageToVideoParams = reactive({
|
||||
image_urls: [],
|
||||
aspect_ratio: '16:9',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const currentFunction = computed(() => {
|
||||
return functions.find((f) => f.key === activeFunction.value) || functions[0]
|
||||
})
|
||||
|
||||
const currentFunctions = computed(() => {
|
||||
return functions.filter((f) => f.category === activeCategory.value)
|
||||
})
|
||||
|
||||
const needsPrompt = computed(() => currentFunction.value.needsPrompt)
|
||||
const needsImage = computed(() => currentFunction.value.needsImage)
|
||||
const needsMultipleImages = computed(() => currentFunction.value.multiple)
|
||||
const currentPowerCost = computed(() => currentFunction.value.power)
|
||||
|
||||
// 初始化方法
|
||||
const init = async () => {
|
||||
try {
|
||||
// 获取算力消耗配置
|
||||
const powerRes = await httpGet('/api/jimeng/power-config')
|
||||
if (powerRes.data) {
|
||||
Object.assign(powerConfig, powerRes.data)
|
||||
setFunctionPowers(powerRes.data)
|
||||
}
|
||||
const user = await checkSession()
|
||||
isLogin.value = true
|
||||
userPower.value = user.power
|
||||
// 获取任务列表
|
||||
await fetchData(1)
|
||||
// 开始轮询
|
||||
startPolling()
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换功能分类
|
||||
const switchCategory = (category) => {
|
||||
activeCategory.value = category
|
||||
const categoryFunctions = functions.filter((f) => f.category === category)
|
||||
if (categoryFunctions.length > 0) {
|
||||
if (category === 'image_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image' : 'text_to_image'
|
||||
} else if (category === 'video_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
} else {
|
||||
activeFunction.value = categoryFunctions[0].key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换输入模式
|
||||
const switchInputMode = () => {
|
||||
if (activeCategory.value === 'image_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image' : 'text_to_image'
|
||||
} else if (activeCategory.value === 'video_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
}
|
||||
}
|
||||
|
||||
// 切换功能
|
||||
const switchFunction = (functionKey) => {
|
||||
activeFunction.value = functionKey
|
||||
}
|
||||
|
||||
// 获取当前算力消耗
|
||||
const getCurrentPowerCost = () => {
|
||||
return currentFunction.value.power
|
||||
}
|
||||
|
||||
// 获取功能名称
|
||||
const getFunctionName = (type) => {
|
||||
const func = functions.find((f) => f.key === type)
|
||||
return func ? func.name : type
|
||||
}
|
||||
|
||||
// 获取任务状态文本
|
||||
const getTaskStatusText = (status) => {
|
||||
const statusMap = {
|
||||
in_queue: '任务排队中',
|
||||
generating: '任务执行中',
|
||||
success: '任务成功',
|
||||
failed: '任务失败',
|
||||
canceled: '任务已取消',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getTaskType = (type) => {
|
||||
const typeMap = {
|
||||
text_to_image: 'primary',
|
||||
image_to_image: 'primary',
|
||||
image_edit: 'primary',
|
||||
image_effects: 'primary',
|
||||
text_to_video: 'success',
|
||||
image_to_video: 'success',
|
||||
}
|
||||
return typeMap[type] || 'primary'
|
||||
}
|
||||
|
||||
// 切换任务筛选
|
||||
const switchTaskFilter = (filter) => {
|
||||
taskFilter.value = filter
|
||||
isOver.value = false
|
||||
fetchData(1)
|
||||
}
|
||||
|
||||
// 轮询定时器
|
||||
let pollHandler = null
|
||||
// 获取任务列表
|
||||
const fetchData = async (pageNum = 1) => {
|
||||
try {
|
||||
loading.value = true
|
||||
page.value = pageNum
|
||||
|
||||
const response = await httpPost('/api/jimeng/jobs', {
|
||||
page: pageNum,
|
||||
page_size: pageSize.value,
|
||||
filter: taskFilter.value,
|
||||
})
|
||||
|
||||
const data = response.data
|
||||
if (!data.items || data.items.length === 0) {
|
||||
isOver.value = true
|
||||
if (pageNum === 1) {
|
||||
currentList.value = []
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
total.value = data.total || 0
|
||||
if (data.items.length < pageSize.value) {
|
||||
isOver.value = true
|
||||
}
|
||||
if (pageNum === 1) {
|
||||
currentList.value = data.items
|
||||
} else {
|
||||
currentList.value = currentList.value.concat(data.items)
|
||||
}
|
||||
} catch (error) {
|
||||
showMessageError('获取任务列表失败:' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 简单轮询逻辑
|
||||
const startPolling = () => {
|
||||
if (pollHandler) {
|
||||
clearInterval(pollHandler)
|
||||
}
|
||||
pollHandler = setInterval(async () => {
|
||||
const response = await httpPost('/api/jimeng/jobs', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
const data = response.data
|
||||
if (data.items.length === 0) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
const todoList = data.items.filter(
|
||||
(item) => item.status === 'in_queue' || item.status === 'generating'
|
||||
)
|
||||
// 更新当前列表
|
||||
currentList.value.forEach((item) => {
|
||||
const index = data.items.findIndex((i) => i.id === item.id)
|
||||
if (index !== -1) {
|
||||
Object.assign(item, data.items[index])
|
||||
}
|
||||
})
|
||||
if (todoList.length === 0) {
|
||||
stopPolling()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollHandler) {
|
||||
clearInterval(pollHandler)
|
||||
pollHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
const submitTask = async () => {
|
||||
if (!isLogin.value) {
|
||||
shareStore.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
if (userPower.value < currentPowerCost.value) {
|
||||
showMessageError('算力不足')
|
||||
return
|
||||
}
|
||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
||||
if (activeFunction.value !== 'image_effects' && !currentPrompt.value) {
|
||||
showMessageError('提示词不能为空')
|
||||
return
|
||||
}
|
||||
try {
|
||||
submitting.value = true
|
||||
let requestData = { task_type: activeFunction.value, prompt: currentPrompt.value }
|
||||
switch (activeFunction.value) {
|
||||
case 'text_to_image':
|
||||
Object.assign(requestData, {
|
||||
width: parseInt(textToImageParams.size.split('x')[0]),
|
||||
height: parseInt(textToImageParams.size.split('x')[1]),
|
||||
scale: textToImageParams.scale,
|
||||
seed: textToImageParams.seed,
|
||||
use_pre_llm: textToImageParams.use_pre_llm,
|
||||
})
|
||||
break
|
||||
case 'image_to_image':
|
||||
Object.assign(requestData, {
|
||||
image_input: imageToImageParams.image_input,
|
||||
width: parseInt(imageToImageParams.size.split('x')[0]),
|
||||
height: parseInt(imageToImageParams.size.split('x')[1]),
|
||||
gpen: imageToImageParams.gpen,
|
||||
skin: imageToImageParams.skin,
|
||||
skin_unifi: imageToImageParams.skin_unifi,
|
||||
gen_mode: imageToImageParams.gen_mode,
|
||||
seed: imageToImageParams.seed,
|
||||
})
|
||||
break
|
||||
case 'image_edit':
|
||||
Object.assign(requestData, {
|
||||
image_urls: imageEditParams.image_urls,
|
||||
scale: imageEditParams.scale,
|
||||
seed: imageEditParams.seed,
|
||||
})
|
||||
break
|
||||
case 'image_effects':
|
||||
Object.assign(requestData, {
|
||||
image_input: imageEffectsParams.image_input1,
|
||||
template_id: imageEffectsParams.template_id,
|
||||
width: parseInt(imageEffectsParams.size.split('x')[0]),
|
||||
height: parseInt(imageEffectsParams.size.split('x')[1]),
|
||||
})
|
||||
break
|
||||
case 'text_to_video':
|
||||
Object.assign(requestData, {
|
||||
aspect_ratio: textToVideoParams.aspect_ratio,
|
||||
seed: textToVideoParams.seed,
|
||||
})
|
||||
break
|
||||
case 'image_to_video':
|
||||
Object.assign(requestData, {
|
||||
image_urls: imageToVideoParams.image_urls,
|
||||
aspect_ratio: imageToVideoParams.aspect_ratio,
|
||||
seed: imageToVideoParams.seed,
|
||||
})
|
||||
break
|
||||
}
|
||||
const response = await httpPost('/api/jimeng/task', requestData)
|
||||
if (response.data) {
|
||||
showMessageOK('任务提交成功')
|
||||
isOver.value = false
|
||||
await fetchData(1)
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交任务失败:', error)
|
||||
showMessageError(error.message || '提交任务失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (item) => {
|
||||
const url = replaceImg(item.video_url || item.img_url)
|
||||
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
|
||||
item.downloading = true
|
||||
|
||||
try {
|
||||
const response = await httpDownload(downloadURL)
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
} catch (error) {
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重试任务
|
||||
const retryTask = async (taskId) => {
|
||||
try {
|
||||
const response = await httpGet(`/api/jimeng/retry?id=${taskId}`)
|
||||
if (response.data) {
|
||||
showMessageOK('重试任务已提交')
|
||||
isOver.value = false
|
||||
await fetchData(1)
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重试任务失败:', error)
|
||||
showMessageError(error.message || '重试任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const removeJob = async (item) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
|
||||
const response = await httpGet('/api/jimeng/remove', { id: item.id })
|
||||
if (response.data) {
|
||||
showMessageOK('删除成功')
|
||||
await fetchData(1)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除任务失败:', error)
|
||||
showMessageError(error.message || '删除任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (item) => {
|
||||
currentVideoUrl.value = item.video_url
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
// 画同款功能
|
||||
const drawSame = (item) => {
|
||||
// 联动功能开关
|
||||
if (item.type === 'text_to_image' || item.type === 'image_to_image') {
|
||||
activeCategory.value = 'image_generation'
|
||||
useImageInput.value = item.type === 'image_to_image'
|
||||
} else if (item.type === 'text_to_video' || item.type === 'image_to_video') {
|
||||
activeCategory.value = 'video_generation'
|
||||
useImageInput.value = item.type === 'image_to_video'
|
||||
} else if (item.type === 'image_edit') {
|
||||
activeCategory.value = 'image_editing'
|
||||
} else if (item.type === 'image_effects') {
|
||||
activeCategory.value = 'image_effects'
|
||||
}
|
||||
switchFunction(item.type)
|
||||
nextTick(() => {
|
||||
currentPrompt.value = item.prompt
|
||||
})
|
||||
if (item.type === 'text_to_image') {
|
||||
if (item.width && item.height) {
|
||||
textToImageParams.size = `${item.width}x${item.height}`
|
||||
}
|
||||
if (item.scale) textToImageParams.scale = item.scale
|
||||
if (item.seed) textToImageParams.seed = item.seed
|
||||
if (item.use_pre_llm !== undefined) textToImageParams.use_pre_llm = item.use_pre_llm
|
||||
} else if (item.type === 'image_to_image') {
|
||||
if (item.image_input) imageToImageParams.image_input = item.image_input
|
||||
if (item.width && item.height) {
|
||||
imageToImageParams.size = `${item.width}x${item.height}`
|
||||
}
|
||||
if (item.gpen) imageToImageParams.gpen = item.gpen
|
||||
if (item.skin) imageToImageParams.skin = item.skin
|
||||
if (item.skin_unifi) imageToImageParams.skin_unifi = item.skin_unifi
|
||||
if (item.gen_mode) imageToImageParams.gen_mode = item.gen_mode
|
||||
if (item.seed) imageToImageParams.seed = item.seed
|
||||
} else if (item.type === 'image_edit') {
|
||||
if (item.image_urls) imageEditParams.image_urls = item.image_urls
|
||||
if (item.scale) imageEditParams.scale = item.scale
|
||||
if (item.seed) imageEditParams.seed = item.seed
|
||||
} else if (item.type === 'image_effects') {
|
||||
if (item.image_input1) imageEffectsParams.image_input1 = item.image_input1
|
||||
if (item.template_id) imageEffectsParams.template_id = item.template_id
|
||||
if (item.width && item.height) {
|
||||
imageEffectsParams.size = `${item.width}x${item.height}`
|
||||
}
|
||||
} else if (item.type === 'text_to_video') {
|
||||
if (item.aspect_ratio) textToVideoParams.aspect_ratio = item.aspect_ratio
|
||||
if (item.seed) textToVideoParams.seed = item.seed
|
||||
} else if (item.type === 'image_to_video') {
|
||||
if (item.image_urls) imageToVideoParams.image_urls = item.image_urls
|
||||
if (item.aspect_ratio) imageToVideoParams.aspect_ratio = item.aspect_ratio
|
||||
if (item.seed) imageToVideoParams.seed = item.seed
|
||||
}
|
||||
showMessageOK('已填入全部参数,可直接生成同款')
|
||||
}
|
||||
|
||||
// 页面卸载时清理轮询
|
||||
const cleanup = () => {
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
// 返回所有状态和方法
|
||||
return {
|
||||
// 状态
|
||||
activeCategory,
|
||||
activeFunction,
|
||||
useImageInput,
|
||||
loading,
|
||||
submitting,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
taskFilter,
|
||||
currentList,
|
||||
isOver,
|
||||
isLogin,
|
||||
userPower,
|
||||
showDialog,
|
||||
currentVideoUrl,
|
||||
|
||||
// 配置
|
||||
categories,
|
||||
functions,
|
||||
currentFunctions,
|
||||
|
||||
// 参数
|
||||
currentPrompt,
|
||||
textToImageParams,
|
||||
imageToImageParams,
|
||||
imageEditParams,
|
||||
imageEffectsParams,
|
||||
textToVideoParams,
|
||||
imageToVideoParams,
|
||||
|
||||
// 计算属性
|
||||
currentFunction,
|
||||
needsPrompt,
|
||||
needsImage,
|
||||
needsMultipleImages,
|
||||
currentPowerCost,
|
||||
|
||||
// 方法
|
||||
init,
|
||||
switchCategory,
|
||||
switchFunction,
|
||||
switchInputMode,
|
||||
getCurrentPowerCost,
|
||||
getFunctionName,
|
||||
getTaskStatusText,
|
||||
getTaskType,
|
||||
switchTaskFilter,
|
||||
fetchData,
|
||||
submitTask,
|
||||
downloadFile,
|
||||
retryTask,
|
||||
removeJob,
|
||||
playVideo,
|
||||
cleanup,
|
||||
drawSame,
|
||||
|
||||
// 工具函数
|
||||
substr,
|
||||
replaceImg,
|
||||
}
|
||||
})
|
||||
|
||||
export const imageSizeOptions = [
|
||||
{ label: '1:1 (1328x1328)', value: '1328x1328' },
|
||||
{ label: '3:2 (1584x1056)', value: '1584x1056' },
|
||||
{ label: '2:3 (1056x1584)', value: '1056x1584' },
|
||||
{ label: '4:3 (1472x1104)', value: '1472x1104' },
|
||||
{ label: '3:4 (1104x1472)', value: '1104x1472' },
|
||||
{ label: '16:9 (1664x936)', value: '1664x936' },
|
||||
{ label: '9:16 (936x1664)', value: '936x1664' },
|
||||
{ label: '21:9 (2016x864)', value: '2016x864' },
|
||||
{ label: '9:21 (864x2016)', value: '864x2016' },
|
||||
]
|
||||
|
||||
export const videoAspectRatioOptions = [
|
||||
{ label: '1:1 (正方形)', value: '1:1' },
|
||||
{ label: '16:9 (横版)', value: '16:9' },
|
||||
{ label: '9:16 (竖版)', value: '9:16' },
|
||||
]
|
||||
602
web/src/store/video.js
Normal file
602
web/src/store/video.js
Normal file
@@ -0,0 +1,602 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr } from '@/utils/libs'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
export const useVideoStore = defineStore('video', () => {
|
||||
// 当前活跃的视频类型
|
||||
const activeVideoType = ref('luma')
|
||||
|
||||
// 共同状态
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const pullHandler = ref(null)
|
||||
const clipboard = ref(null)
|
||||
|
||||
// 视频预览
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
|
||||
// 用户信息
|
||||
const isLogin = ref(false)
|
||||
const availablePower = ref(100)
|
||||
const shareStore = useSharedStore()
|
||||
|
||||
// 任务筛选
|
||||
const taskFilter = ref('all') // 'all', 'luma', 'keling'
|
||||
|
||||
// Luma 相关状态
|
||||
const lumaUseImageMode = ref(false) // 是否使用图片辅助生成
|
||||
const lumaParams = reactive({
|
||||
prompt: '',
|
||||
expand_prompt: false,
|
||||
loop: false,
|
||||
image: '', // 起始帧
|
||||
image_tail: '', // 结束帧
|
||||
})
|
||||
|
||||
// KeLing 相关状态
|
||||
const isGenerating = ref(false)
|
||||
const generating = ref(false)
|
||||
const kelingPowerCost = ref(10)
|
||||
const lumaPowerCost = ref(10)
|
||||
const showCameraControl = ref(false)
|
||||
const keLingPowers = ref({})
|
||||
|
||||
const models = ref([
|
||||
{ text: '可灵 1.6', value: 'kling-v1-6' },
|
||||
{ text: '可灵 1.5', value: 'kling-v1-5' },
|
||||
{ text: '可灵 1.0', value: 'kling-v1' },
|
||||
])
|
||||
|
||||
const rates = [
|
||||
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.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' },
|
||||
]
|
||||
|
||||
// KeLing 相关状态
|
||||
const kelingUseImageMode = ref(false) // 是否使用图片辅助生成
|
||||
const kelingParams = reactive({
|
||||
model: 'kling-v1-6',
|
||||
prompt: '',
|
||||
negative_prompt: '',
|
||||
cfg_scale: 0.7,
|
||||
mode: 'std',
|
||||
aspect_ratio: '16:9',
|
||||
duration: '5',
|
||||
camera_control: {
|
||||
type: '',
|
||||
config: {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
pan: 0,
|
||||
tilt: 0,
|
||||
roll: 0,
|
||||
zoom: 0,
|
||||
},
|
||||
},
|
||||
image: '',
|
||||
image_tail: '',
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const currentList = computed(() => {
|
||||
return list.value.filter((item) => {
|
||||
if (taskFilter.value === 'all') {
|
||||
return true
|
||||
} else if (taskFilter.value === 'luma') {
|
||||
return item.type === 'luma' || !item.type // 兼容旧数据
|
||||
} else if (taskFilter.value === 'keling') {
|
||||
return item.type === 'keling'
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化方法
|
||||
const init = async () => {
|
||||
try {
|
||||
const user = await checkSession()
|
||||
isLogin.value = true
|
||||
availablePower.value = user.power
|
||||
|
||||
// 初始化剪贴板
|
||||
if (clipboard.value) {
|
||||
clipboard.value.destroy()
|
||||
}
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
|
||||
// 获取系统信息
|
||||
const sysInfo = await getSystemInfo()
|
||||
lumaPowerCost.value = sysInfo.data.luma_power
|
||||
keLingPowers.value = sysInfo.data.keling_powers
|
||||
updateModelPower()
|
||||
|
||||
// 获取数据并开始轮询
|
||||
await fetchData(1)
|
||||
startPolling()
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理方法
|
||||
const cleanup = () => {
|
||||
if (clipboard.value) {
|
||||
clipboard.value.destroy()
|
||||
}
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
const startPolling = () => {
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(page.value)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
const stopPolling = () => {
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
pullHandler.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
const fetchData = async (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: taskFilter.value === 'all' ? '' : taskFilter.value,
|
||||
})
|
||||
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true
|
||||
}
|
||||
items.push({
|
||||
...v,
|
||||
downloading: false,
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
console.error('获取任务列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Luma 相关方法
|
||||
const uploadLumaStartImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
|
||||
try {
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
lumaParams.image = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
closeLoading()
|
||||
} catch (error) {
|
||||
showMessageError('上传失败: ' + error.message)
|
||||
closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const uploadLumaEndImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
|
||||
try {
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
lumaParams.image_tail = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
showMessageError('上传失败: ' + error.message)
|
||||
} finally {
|
||||
closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const removeLumaImage = (type) => {
|
||||
if (type === 'start') {
|
||||
lumaParams.image = ''
|
||||
} else if (type === 'end') {
|
||||
lumaParams.image_tail = ''
|
||||
}
|
||||
}
|
||||
|
||||
const switchLumaImages = () => {
|
||||
;[lumaParams.image, lumaParams.image_tail] = [lumaParams.image_tail, lumaParams.image]
|
||||
}
|
||||
|
||||
const toggleLumaImageMode = (enabled) => {
|
||||
lumaUseImageMode.value = enabled
|
||||
// 关闭时清空图片
|
||||
if (!enabled) {
|
||||
lumaParams.image = ''
|
||||
lumaParams.image_tail = ''
|
||||
}
|
||||
}
|
||||
|
||||
const createLumaVideo = async () => {
|
||||
if (!isLogin.value) {
|
||||
shareStore.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!lumaParams.prompt?.trim()) {
|
||||
return ElMessage.error('请输入视频描述')
|
||||
}
|
||||
|
||||
if (lumaUseImageMode.value && !lumaParams.image) {
|
||||
return ElMessage.error('请上传起始帧图片')
|
||||
}
|
||||
|
||||
// 处理参数
|
||||
const requestData = {
|
||||
...lumaParams,
|
||||
task_type: lumaUseImageMode.value ? 'image2video' : 'text2video',
|
||||
}
|
||||
|
||||
// 处理图片链接
|
||||
if (requestData.image) {
|
||||
requestData.first_frame_img = replaceImg(requestData.image)
|
||||
}
|
||||
if (requestData.image_tail) {
|
||||
requestData.end_frame_img = replaceImg(requestData.image_tail)
|
||||
}
|
||||
|
||||
try {
|
||||
await httpPost('/api/video/luma/create', requestData)
|
||||
await fetchData(1)
|
||||
taskPulling.value = true
|
||||
showMessageOK('创建任务成功')
|
||||
} catch (error) {
|
||||
showMessageError('创建任务失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// KeLing 相关方法
|
||||
const changeRate = (item) => {
|
||||
kelingParams.aspect_ratio = item.value
|
||||
}
|
||||
|
||||
const updateModelPower = () => {
|
||||
showCameraControl.value = kelingParams.model === 'kling-v1-5' && kelingParams.mode === 'pro'
|
||||
kelingPowerCost.value =
|
||||
keLingPowers.value[`${kelingParams.model}_${kelingParams.mode}_${kelingParams.duration}`] ||
|
||||
10
|
||||
}
|
||||
|
||||
const toggleKelingImageMode = (enabled) => {
|
||||
kelingUseImageMode.value = enabled
|
||||
// 关闭时清空图片
|
||||
if (!enabled) {
|
||||
kelingParams.image = ''
|
||||
kelingParams.image_tail = ''
|
||||
}
|
||||
}
|
||||
|
||||
const uploadKelingStartImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
|
||||
try {
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
kelingParams.image = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
closeLoading()
|
||||
} catch (error) {
|
||||
showMessageError('上传失败: ' + error.message)
|
||||
closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const uploadKelingEndImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
|
||||
try {
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
kelingParams.image_tail = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
showMessageError('上传失败: ' + error.message)
|
||||
} finally {
|
||||
closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const removeKelingImage = (type) => {
|
||||
if (type === 'start') {
|
||||
kelingParams.image = ''
|
||||
} else if (type === 'end') {
|
||||
kelingParams.image_tail = ''
|
||||
}
|
||||
}
|
||||
|
||||
const switchKelingImages = () => {
|
||||
;[kelingParams.image, kelingParams.image_tail] = [kelingParams.image_tail, kelingParams.image]
|
||||
}
|
||||
|
||||
const createKelingVideo = async () => {
|
||||
if (!isLogin.value) {
|
||||
shareStore.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (generating.value) return
|
||||
|
||||
if (!kelingParams.prompt?.trim()) {
|
||||
return ElMessage.error('请输入视频描述')
|
||||
}
|
||||
|
||||
if (kelingParams.prompt.length > 500) {
|
||||
return ElMessage.error('视频描述不能超过 500 个字符')
|
||||
}
|
||||
|
||||
if (kelingUseImageMode.value && !kelingParams.image) {
|
||||
return ElMessage.error('请上传起始帧图片')
|
||||
}
|
||||
|
||||
generating.value = true
|
||||
|
||||
// 处理参数
|
||||
const requestData = {
|
||||
...kelingParams,
|
||||
task_type: kelingUseImageMode.value ? 'image2video' : 'text2video',
|
||||
}
|
||||
|
||||
// 处理图片链接
|
||||
if (requestData.image) {
|
||||
requestData.image = replaceImg(requestData.image)
|
||||
}
|
||||
if (requestData.image_tail) {
|
||||
requestData.image_tail = replaceImg(requestData.image_tail)
|
||||
}
|
||||
|
||||
try {
|
||||
await httpPost('/api/video/keling/create', requestData)
|
||||
showMessageOK('任务创建成功')
|
||||
|
||||
// 新增重置
|
||||
page.value = 1
|
||||
list.value.unshift({
|
||||
progress: 0,
|
||||
prompt: requestData.prompt,
|
||||
raw_data: {
|
||||
task_type: requestData.task_type,
|
||||
model: requestData.model,
|
||||
duration: requestData.duration,
|
||||
mode: requestData.mode,
|
||||
},
|
||||
})
|
||||
taskPulling.value = true
|
||||
} catch (error) {
|
||||
showMessageError('创建失败: ' + error.message)
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提示词生成
|
||||
const generatePrompt = async () => {
|
||||
if (isGenerating.value) return
|
||||
|
||||
const prompt = activeVideoType.value === 'luma' ? lumaParams.prompt : kelingParams.prompt
|
||||
if (!prompt) {
|
||||
return showMessageError('请输入原始提示词')
|
||||
}
|
||||
|
||||
isGenerating.value = true
|
||||
showLoading('正在生成视频脚本...')
|
||||
|
||||
try {
|
||||
const res = await httpPost('/api/prompt/video', { prompt })
|
||||
if (activeVideoType.value === 'luma') {
|
||||
lumaParams.prompt = res.data
|
||||
} else {
|
||||
kelingParams.prompt = res.data
|
||||
}
|
||||
closeLoading()
|
||||
} catch (error) {
|
||||
showMessageError('生成提示词失败:' + error.message)
|
||||
closeLoading()
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 视频预览
|
||||
const playVideo = (item) => {
|
||||
currentVideoUrl.value = replaceImg(item.video_url)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
// 视频下载
|
||||
const downloadVideo = async (item) => {
|
||||
const url = replaceImg(item.video_url)
|
||||
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
|
||||
item.downloading = true
|
||||
|
||||
try {
|
||||
const response = await httpDownload(downloadURL)
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
} catch (error) {
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const removeJob = async (item) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
|
||||
await httpGet('/api/video/remove', { id: item.id })
|
||||
ElMessage.success('任务删除成功')
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('任务删除失败:' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发布任务
|
||||
const publishJob = async (item) => {
|
||||
try {
|
||||
await httpGet('/api/video/publish', { id: item.id, publish: item.publish })
|
||||
ElMessage.success('操作成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换视频类型
|
||||
const switchVideoType = (type) => {
|
||||
activeVideoType.value = type
|
||||
}
|
||||
|
||||
// 切换任务筛选
|
||||
const switchTaskFilter = (filter) => {
|
||||
taskFilter.value = filter
|
||||
page.value = 1
|
||||
fetchData(1)
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
activeVideoType,
|
||||
loading,
|
||||
list,
|
||||
currentList,
|
||||
noData,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
taskPulling,
|
||||
showDialog,
|
||||
currentVideoUrl,
|
||||
isLogin,
|
||||
availablePower,
|
||||
nodata,
|
||||
taskFilter,
|
||||
|
||||
// Luma 状态
|
||||
lumaUseImageMode,
|
||||
lumaParams,
|
||||
lumaPowerCost,
|
||||
// KeLing 状态
|
||||
kelingUseImageMode,
|
||||
isGenerating,
|
||||
generating,
|
||||
kelingPowerCost,
|
||||
showCameraControl,
|
||||
keLingPowers,
|
||||
models,
|
||||
rates,
|
||||
kelingParams,
|
||||
|
||||
// 方法
|
||||
init,
|
||||
cleanup,
|
||||
fetchData,
|
||||
switchVideoType,
|
||||
switchTaskFilter,
|
||||
|
||||
// Luma 方法
|
||||
toggleLumaImageMode,
|
||||
uploadLumaStartImage,
|
||||
uploadLumaEndImage,
|
||||
removeLumaImage,
|
||||
switchLumaImages,
|
||||
createLumaVideo,
|
||||
|
||||
// KeLing 方法
|
||||
toggleKelingImageMode,
|
||||
changeRate,
|
||||
updateModelPower,
|
||||
uploadKelingStartImage,
|
||||
uploadKelingEndImage,
|
||||
removeKelingImage,
|
||||
switchKelingImages,
|
||||
createKelingVideo,
|
||||
|
||||
// 共同方法
|
||||
generatePrompt,
|
||||
playVideo,
|
||||
downloadVideo,
|
||||
removeJob,
|
||||
publishJob,
|
||||
substr,
|
||||
replaceImg,
|
||||
}
|
||||
})
|
||||
@@ -255,3 +255,8 @@ export function isChrome() {
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
return /chrome/.test(userAgent) && !/edg/.test(userAgent)
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
export function formatDateTime(timestamp, format = 'yyyy-MM-dd HH:mm:ss') {
|
||||
return dateFormat(timestamp, format)
|
||||
}
|
||||
|
||||
@@ -219,8 +219,8 @@
|
||||
{{ model.power > 0 ? `${model.power}算力` : '免费' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="model-description" :title="model.description || '暂无描述'">
|
||||
{{ model.description || '暂无描述' }}
|
||||
<div class="model-description" :title="model.desc || '暂无描述'">
|
||||
{{ model.desc || '暂无描述' }}
|
||||
</div>
|
||||
<!-- 暂时屏蔽此信息展示,或许用户不想展示此信息 -->
|
||||
<div class="model-metadata">
|
||||
@@ -299,7 +299,7 @@
|
||||
v-model="prompt"
|
||||
@keydown="onInput"
|
||||
@input="onInput"
|
||||
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
|
||||
placeholder="按 Enter 键发送消息,使用 Shift + Enter 换行"
|
||||
autofocus
|
||||
>
|
||||
</textarea>
|
||||
@@ -488,7 +488,7 @@ const filteredModels = computed(() => {
|
||||
model.description.toLowerCase().includes(modelSearchKeyword.value.toLowerCase()))
|
||||
|
||||
// 分类匹配
|
||||
const matchesCategory = !activeCategory.value || model.category === activeCategory.value
|
||||
const matchesCategory = !activeCategory.value || model.tag === activeCategory.value
|
||||
|
||||
// 免费模型匹配
|
||||
const matchesFree = !showFreeModelsOnly.value || model.power <= 0
|
||||
@@ -514,8 +514,8 @@ const toggleFreeModels = () => {
|
||||
const updateModelCategories = () => {
|
||||
const categories = new Set()
|
||||
models.value.forEach((model) => {
|
||||
if (model.category) {
|
||||
categories.add(model.category)
|
||||
if (model.tag) {
|
||||
categories.add(model.tag)
|
||||
}
|
||||
})
|
||||
modelCategories.value = Array.from(categories)
|
||||
@@ -539,7 +539,7 @@ const updateGroupedModels = () => {
|
||||
// 否则按分类分组展示
|
||||
const groups = {}
|
||||
filtered.forEach((model) => {
|
||||
const category = model.category || '未分类'
|
||||
const category = model.tag || '未分类'
|
||||
if (!groups[category]) {
|
||||
groups[category] = []
|
||||
}
|
||||
@@ -1092,9 +1092,8 @@ const onInput = (e) => {
|
||||
|
||||
// 输入回车自动提交
|
||||
if (e.keyCode === 13) {
|
||||
if (e.ctrlKey) {
|
||||
// Ctrl + Enter 换行
|
||||
prompt.value += '\n'
|
||||
// Shift + Enter 换行
|
||||
if (e.shiftKey) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
|
||||
<template #reference>
|
||||
<li class="menu-list-item flex-center-col">
|
||||
<i class="iconfont icon-config" />
|
||||
<i class="iconfont icon-user-circle" />
|
||||
</li>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -97,6 +97,11 @@
|
||||
</ul>
|
||||
</template>
|
||||
</el-popover>
|
||||
<div v-else class="mb-2 flex justify-center">
|
||||
<el-button @click="store.setShowLoginDialog(true)" type="primary" size="small">
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="menu-bot-item">
|
||||
<a @click="router.push('/')" class="link-button">
|
||||
<i class="iconfont icon-house"></i>
|
||||
@@ -109,14 +114,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="right-main">
|
||||
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
||||
<!-- <div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
||||
<el-button
|
||||
@click="router.push('/login')"
|
||||
class="btn-go animate__animated animate__pulse animate__infinite"
|
||||
round
|
||||
>登录</el-button
|
||||
>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="content custom-scroll">
|
||||
<router-view :key="routerViewKey" v-slot="{ Component }">
|
||||
<transition name="move" mode="out-in">
|
||||
@@ -209,7 +214,6 @@ watch(
|
||||
// 监听路由变化;
|
||||
router.beforeEach((to, from, next) => {
|
||||
curPath.value = to.path
|
||||
console.log(curPath.value)
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -281,7 +285,9 @@ const logout = function () {
|
||||
httpGet('/api/user/logout')
|
||||
.then(() => {
|
||||
removeUserToken()
|
||||
router.push('/login')
|
||||
// 刷新组件
|
||||
routerViewKey.value += 1
|
||||
loginUser.value = {}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('注销失败!')
|
||||
|
||||
@@ -688,87 +688,26 @@
|
||||
v-if="item.progress === 100"
|
||||
>
|
||||
<div class="opt" v-if="item['can_opt']">
|
||||
<div class="flex flex-row justify-start items-center mb-3">
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600"
|
||||
@click="upscale(1, item)"
|
||||
>
|
||||
U1
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600 ml-2"
|
||||
@click="upscale(2, item)"
|
||||
>
|
||||
U2
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600 ml-2"
|
||||
@click="upscale(3, item)"
|
||||
>
|
||||
U3
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600 ml-2"
|
||||
@click="upscale(4, item)"
|
||||
>
|
||||
U4
|
||||
</button>
|
||||
|
||||
<div class="show-prompt ml-2">
|
||||
<el-popover
|
||||
placement="left"
|
||||
title="提示词"
|
||||
:width="240"
|
||||
trigger="hover"
|
||||
<el-row :gutter="8" class="mb-3">
|
||||
<el-col :span="6" v-for="i in 4" :key="'u' + i">
|
||||
<button
|
||||
class="w-full h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600"
|
||||
@click="upscale(i, item)"
|
||||
>
|
||||
<template #reference>
|
||||
<i class="iconfont icon-prompt text-white text-xl"></i>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="mj-list-item-prompt">
|
||||
<span>{{ item.prompt }}</span>
|
||||
<el-icon
|
||||
class="copy-prompt-mj"
|
||||
:data-clipboard-text="item.prompt"
|
||||
>
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-start items-center mb-3">
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600"
|
||||
@click="variation(1, item)"
|
||||
>
|
||||
V1
|
||||
</button>
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600 ml-2"
|
||||
@click="variation(2, item)"
|
||||
>
|
||||
V2
|
||||
</button>
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600 ml-2"
|
||||
@click="variation(3, item)"
|
||||
>
|
||||
V3
|
||||
</button>
|
||||
<button
|
||||
class="px-3 h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600 ml-2"
|
||||
@click="variation(4, item)"
|
||||
>
|
||||
V4
|
||||
</button>
|
||||
</div>
|
||||
U{{ i }}
|
||||
</button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="8" class="mb-3">
|
||||
<el-col :span="6" v-for="i in 4" :key="'v' + i">
|
||||
<button
|
||||
class="w-full h-6 rounded bg-gray-500 text-xs text-white shadow-md transition-all duration-300 hover:bg-gray-600"
|
||||
@click="variation(i, item)"
|
||||
>
|
||||
V{{ i }}
|
||||
</button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -797,13 +736,28 @@
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage(item)"
|
||||
circle
|
||||
/>
|
||||
<el-button type="danger" @click="removeImage(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-popover
|
||||
placement="top"
|
||||
title="提示词"
|
||||
:width="240"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="primary" circle>
|
||||
<i class="iconfont icon-prompt text-white"></i>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="mj-list-item-prompt">
|
||||
<span>{{ item.prompt }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -866,7 +820,7 @@ import { useSharedStore } from '@/store/sharedata'
|
||||
import { closeLoading, showLoading, showMessageError } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { copyObj, removeArrayItem } from '@/utils/libs'
|
||||
import { Delete, DocumentCopy, InfoFilled, Plus, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { Delete, InfoFilled, Plus, UploadFilled } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import Compressor from 'compressorjs'
|
||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
class="nav-item-box"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<i :class="'iconfont ' + iconMap[item.url]"></i>
|
||||
<i :class="'iconfont ' + item.icon"></i>
|
||||
<div>{{ item.name }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
@@ -107,20 +107,6 @@ const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
|
||||
const navs = ref([])
|
||||
|
||||
const iconMap = ref({
|
||||
'/chat': 'icon-chat',
|
||||
'/mj': 'icon-mj',
|
||||
'/sd': 'icon-sd',
|
||||
'/dalle': 'icon-dalle',
|
||||
'/images-wall': 'icon-image',
|
||||
'/suno': 'icon-suno',
|
||||
'/xmind': 'icon-xmind',
|
||||
'/apps': 'icon-app',
|
||||
'/member': 'icon-vip-user',
|
||||
'/invite': 'icon-share',
|
||||
'/luma': 'icon-luma',
|
||||
})
|
||||
|
||||
const displayedChars = ref([])
|
||||
const initAnimation = ref('')
|
||||
let timer = null // 定时器句柄
|
||||
|
||||
688
web/src/views/Jimeng.vue
Normal file
688
web/src/views/Jimeng.vue
Normal file
@@ -0,0 +1,688 @@
|
||||
<template>
|
||||
<div class="page-jimeng">
|
||||
<!-- 左侧参数设置面板 -->
|
||||
<div class="params-panel">
|
||||
<!-- 功能分类按钮组 -->
|
||||
<div class="category-buttons">
|
||||
<div class="category-grid">
|
||||
<div
|
||||
v-for="category in store.categories"
|
||||
:key="category.key"
|
||||
:class="['category-btn', { active: store.activeCategory === category.key }]"
|
||||
@click="store.switchCategory(category.key)"
|
||||
>
|
||||
<div class="category-icon">
|
||||
<i :class="getCategoryIcon(category.key)"></i>
|
||||
</div>
|
||||
<div class="category-name">{{ category.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能开关 -->
|
||||
<div
|
||||
class="function-switch"
|
||||
v-if="
|
||||
store.activeCategory === 'image_generation' || store.activeCategory === 'video_generation'
|
||||
"
|
||||
>
|
||||
<div class="switch-label">
|
||||
<el-icon><Switch /></el-icon>
|
||||
生成模式
|
||||
</div>
|
||||
<div class="switch-container">
|
||||
<div class="switch-info">
|
||||
<div class="switch-title">
|
||||
{{ store.activeCategory === 'image_generation' ? '图生图人像写真' : '图生视频' }}
|
||||
</div>
|
||||
</div>
|
||||
<el-switch v-model="store.useImageInput" @change="store.switchInputMode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数容器 -->
|
||||
<div class="params-container">
|
||||
<!-- 文生图 -->
|
||||
<div v-if="store.activeFunction === 'text_to_image'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="请输入图片描述,越详细越好"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">图片尺寸:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.textToImageParams.size" placeholder="选择尺寸">
|
||||
<el-option
|
||||
v-for="opt in imageSizeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<span class="label"
|
||||
>创意度
|
||||
<el-tooltip content="创意度越高,影响文本描述的程度越高" placement="top">
|
||||
<i class="iconfont icon-info cursor-pointer ml-1"></i> </el-tooltip
|
||||
></span>
|
||||
</div>
|
||||
<div class="item-group">
|
||||
<el-slider v-model="store.textToImageParams.scale" :min="1" :max="10" :step="0.5" />
|
||||
</div>
|
||||
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">智能优化提示词</span>
|
||||
<el-switch v-model="store.textToImageParams.use_pre_llm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生图 -->
|
||||
<div v-if="store.activeFunction === 'image_to_image'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload
|
||||
v-model="store.imageToImageParams.image_input"
|
||||
:max-count="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的图片效果"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">图片尺寸:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageToImageParams.size" placeholder="选择尺寸">
|
||||
<el-option
|
||||
v-for="opt in imageSizeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像编辑 -->
|
||||
<div v-if="store.activeFunction === 'image_edit'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload
|
||||
v-model="store.imageEditParams.image_urls"
|
||||
:max-count="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">编辑提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的编辑效果"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">编辑强度:</span>
|
||||
<el-slider v-model="store.imageEditParams.scale" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像特效 -->
|
||||
<div v-if="store.activeFunction === 'image_effects'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload
|
||||
v-model="store.imageEffectsParams.image_input1"
|
||||
:max-count="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">特效模板:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageEffectsParams.template_id" placeholder="选择特效模板">
|
||||
<el-option label="经典特效" value="classic" />
|
||||
<el-option label="艺术风格" value="artistic" />
|
||||
<el-option label="现代科技" value="modern" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">输出尺寸:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageEffectsParams.size" placeholder="选择尺寸">
|
||||
<el-option
|
||||
v-for="opt in imageSizeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文生视频 -->
|
||||
<div v-if="store.activeFunction === 'text_to_video'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的视频内容"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">视频比例:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.textToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||
<el-option
|
||||
v-for="opt in videoAspectRatioOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生视频 -->
|
||||
<div v-if="store.activeFunction === 'image_to_video'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload
|
||||
v-model="store.imageToVideoParams.image_urls"
|
||||
:max-count="2"
|
||||
:multiple="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的视频效果"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">视频比例:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||
<el-option
|
||||
v-for="opt in videoAspectRatioOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="submit-btn flex justify-center pt-4">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="store.submitTask"
|
||||
:loading="store.submitting"
|
||||
size="large"
|
||||
>
|
||||
立即生成 ({{ store.currentPowerCost }}<i class="iconfont icon-vip2"></i>)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧任务列表 -->
|
||||
<div class="main-content">
|
||||
<div class="works-header">
|
||||
<h2 class="h-title">你的作品</h2>
|
||||
<div class="filter-buttons">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'all' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('all')"
|
||||
size="small"
|
||||
>
|
||||
全部
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'image' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('image')"
|
||||
size="small"
|
||||
>
|
||||
图片
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'video' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('video')"
|
||||
size="small"
|
||||
>
|
||||
视频
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-list" v-loading="store.loading">
|
||||
<div v-if="store.currentList.length > 0">
|
||||
<Waterfall
|
||||
:list="store.currentList"
|
||||
v-bind="waterfallOptions"
|
||||
:is-loading="store.loading"
|
||||
:is-over="store.isOver"
|
||||
:lazyload="true"
|
||||
@afterRender="onWaterfallAfterRender"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="task-item">
|
||||
<!-- 保持原有内容 -->
|
||||
<div class="task-left">
|
||||
<div class="task-preview">
|
||||
<el-image
|
||||
v-if="item.img_url"
|
||||
:src="item.img_url"
|
||||
:preview-src-list="[item.img_url]"
|
||||
:preview-teleported="true"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<img :src="loadingIcon" class="max-w-[50px] max-h-[50px]" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else-if="item.video_url" class="w-full h-full preview-video-wrapper">
|
||||
<video
|
||||
:src="item.video_url"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
class="preview-video w-full h-full"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div class="video-mask" @click="store.playVideo(item)">
|
||||
<div class="play-btn">
|
||||
<img src="/images/play.svg" alt="播放" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="preview-placeholder">
|
||||
<div
|
||||
v-if="item.status === 'in_queue'"
|
||||
class="flex flex-col items-center gap-1"
|
||||
>
|
||||
<i class="iconfont icon-video" v-if="item.type.includes('video')"></i>
|
||||
<i class="iconfont icon-dalle" v-else></i>
|
||||
<span>
|
||||
{{ store.getTaskStatusText(item.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.status === 'generating'"
|
||||
class="flex flex-col items-center gap-1"
|
||||
>
|
||||
<span>
|
||||
<Generating>
|
||||
<div class="text-gray-400 text-base pt-3">
|
||||
{{ store.getTaskStatusText(item.status) }}
|
||||
</div></Generating
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.status === 'failed'"
|
||||
class="flex flex-col items-center gap-1"
|
||||
>
|
||||
<i class="iconfont icon-error text-red-500"></i>
|
||||
<span class="text text-red-500">
|
||||
{{ store.getTaskStatusText(item.status) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="text-sm text-red-400 err-msg-clip cursor-pointer mx-5"
|
||||
@click="copyErrorMsg(item.err_msg)"
|
||||
>
|
||||
{{ item.err_msg }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-center">
|
||||
<div class="task-info flex justify-between">
|
||||
<div class="flex gap-2">
|
||||
<el-tag size="small" :type="store.getTaskType(item.type)">
|
||||
{{ store.getFunctionName(item.type) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span>
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<i
|
||||
class="iconfont icon-copy cursor-pointer"
|
||||
@click="copyPrompt(item.prompt)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="ml-1">
|
||||
<el-tooltip content="画同款" placement="top">
|
||||
<i
|
||||
class="iconfont icon-image-list cursor-pointer"
|
||||
@click="store.drawSame(item)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<template v-if="item.status === 'failed'">
|
||||
<span class="ml-1" v-if="item.status === 'failed'">
|
||||
<el-tooltip content="重试" placement="top">
|
||||
<i
|
||||
class="iconfont icon-refresh cursor-pointer"
|
||||
@click="store.retryTask(item.id)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="ml-1" v-if="item.status === 'failed'">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<i
|
||||
class="iconfont icon-remove cursor-pointer text-red-500"
|
||||
@click="store.removeJob(item)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<span class="ml-1" v-if="item.video_url || item.img_url">
|
||||
<el-tooltip content="下载" placement="top">
|
||||
<i
|
||||
v-if="!item.downloading"
|
||||
class="iconfont icon-download text-sm cursor-pointer"
|
||||
@click="store.downloadFile(item)"
|
||||
></i>
|
||||
<el-image src="/images/loading.gif" class="w-4 h-4" fit="cover" v-else />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="task-prompt line-clamp-2 min-h-[40px] text-[14px] text-theme mb-2 leading-snug break-all"
|
||||
>
|
||||
{{ store.substr(item.prompt, 200) }}
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>{{ dateFormat(item.created_at) }}</span>
|
||||
<span v-if="item.power">{{ item.power }}算力</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
<div class="flex justify-center py-10">
|
||||
<img
|
||||
:src="waterfallOptions.loadProps.loading"
|
||||
class="max-w-[50px] max-h-[50px]"
|
||||
v-if="!waterfallRendered"
|
||||
/>
|
||||
<div v-else>
|
||||
<div class="no-more-data" v-if="store.isOver">
|
||||
<span class="text-gray-500 mr-2">没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else :image-size="100" description="暂无记录" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<el-dialog v-model="store.showDialog" title="视频预览" width="70%" center>
|
||||
<video
|
||||
:src="store.currentVideoUrl"
|
||||
autoplay="true"
|
||||
controls
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import '@/assets/css/jimeng.styl'
|
||||
import loadingIcon from '@/assets/img/loading.gif'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { imageSizeOptions, useJimengStore, videoAspectRatioOptions } from '@/store/jimeng'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import { Switch } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
const sharedStore = useSharedStore()
|
||||
const waterfallOptions = sharedStore.waterfallOptions
|
||||
|
||||
// 获取分类图标
|
||||
const getCategoryIcon = (category) => {
|
||||
const iconMap = {
|
||||
image_generation: 'iconfont icon-image',
|
||||
image_editing: 'iconfont icon-edit',
|
||||
image_effects: 'iconfont icon-chuangzuo',
|
||||
video_generation: 'iconfont icon-video',
|
||||
}
|
||||
return iconMap[category] || 'iconfont icon-image'
|
||||
}
|
||||
|
||||
const store = useJimengStore()
|
||||
|
||||
// 新增:瀑布流渲染完成状态
|
||||
const waterfallRendered = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.cleanup()
|
||||
})
|
||||
|
||||
// 监听 loading,每次 loading 变为 true 时重置渲染状态
|
||||
watch(
|
||||
() => store.loading,
|
||||
(val) => {
|
||||
if (val) {
|
||||
waterfallRendered.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => store.isOver,
|
||||
(val) => {
|
||||
if (val) {
|
||||
waterfallRendered.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function onWaterfallAfterRender() {
|
||||
waterfallRendered.value = true
|
||||
if (!store.loading && !store.isOver) {
|
||||
store.fetchData(store.page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function copyPrompt(prompt) {
|
||||
navigator.clipboard
|
||||
.writeText(prompt)
|
||||
.then(() => {
|
||||
ElMessage.success('提示词已复制')
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
function copyErrorMsg(msg) {
|
||||
navigator.clipboard
|
||||
.writeText(msg)
|
||||
.then(() => {
|
||||
ElMessage.success('错误信息已复制')
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.task-list {
|
||||
.task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
// 新增:增强任务项悬停动画
|
||||
.task-item {
|
||||
transition: box-shadow 3s cubic-bezier(0.4,0,0.2,1), transform 0.5s cubic-bezier(0.4,0,0.2,1), border-color 0.5s;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.task-item:hover {
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.18), 0 1.5px 8px rgba(0,0,0,0.10);
|
||||
border-color: #a259ff;
|
||||
transform: scale(1.025) translateY(-2px);
|
||||
z-index: 10;
|
||||
background: #f7fbff;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.preview-video-wrapper
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
.video-mask
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: rgba(0,0,0,0.25)
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
opacity: 0
|
||||
transition: opacity 0.2s
|
||||
z-index: 2
|
||||
&:hover .video-mask
|
||||
opacity: 1
|
||||
.play-btn
|
||||
width: 64px
|
||||
height: 64px
|
||||
background: rgba(255,255,255,0.3)
|
||||
border-radius: 50%
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15)
|
||||
cursor: pointer
|
||||
z-index: 3
|
||||
transition: background 0.2s
|
||||
&:hover
|
||||
background: rgba(255,255,255,0.4)
|
||||
.play-btn img
|
||||
width: 36px
|
||||
height: 36px
|
||||
.err-msg-clip
|
||||
display: -webkit-box
|
||||
-webkit-line-clamp: 2
|
||||
-webkit-box-orient: vertical
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
word-break: break-all
|
||||
white-space: normal
|
||||
cursor: pointer
|
||||
</style>
|
||||
@@ -1,745 +0,0 @@
|
||||
<template>
|
||||
<div class="page-keling">
|
||||
<div class="inner custom-scroll">
|
||||
<!-- 左侧参数设置面板 -->
|
||||
<el-scrollbar max-height="100vh">
|
||||
<div class="mj-box">
|
||||
<h2>视频参数设置</h2>
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<!-- 画面比例 -->
|
||||
<div class="param-line">
|
||||
<div class="param-line pt">
|
||||
<span>画面比例:</span>
|
||||
<el-tooltip content="生成画面的尺寸比例" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
class="flex-col items-center"
|
||||
:class="
|
||||
item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'
|
||||
"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<el-image class="icon proportion" :src="item.img" fit="cover"></el-image>
|
||||
<div class="texts">{{ item.text }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="模型选择">
|
||||
<el-select
|
||||
v-model="params.model"
|
||||
placeholder="请选择模型"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.value"
|
||||
:label="item.text"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 视频时长 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="视频时长">
|
||||
<el-select
|
||||
v-model="params.duration"
|
||||
placeholder="请选择时长"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option label="5秒" value="5" />
|
||||
<el-option label="10秒" value="10" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 生成模式 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="生成模式">
|
||||
<el-select
|
||||
v-model="params.mode"
|
||||
placeholder="请选择模式"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option label="标准模式" value="std" />
|
||||
<el-option label="专业模式" value="pro" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 创意程度 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="创意程度">
|
||||
<el-slider v-model="params.cfg_scale" :min="0" :max="1" :step="0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 运镜控制 -->
|
||||
<div class="param-line" v-if="showCameraControl">
|
||||
<div class="param-line pt">
|
||||
<span>运镜控制:</span>
|
||||
<el-tooltip content="生成画面的运镜效果,仅 1.5的高级模式可用" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 添加运镜类型选择 -->
|
||||
<div class="param-line">
|
||||
<el-select v-model="params.camera_control.type" placeholder="请选择运镜类型">
|
||||
<el-option label="请选择" value="" />
|
||||
<el-option label="简单运镜" value="simple" />
|
||||
<el-option label="下移拉远" value="down_back" />
|
||||
<el-option label="推进上移" value="forward_up" />
|
||||
<el-option label="右旋推进" value="right_turn_forward" />
|
||||
<el-option label="左旋推进" value="left_turn_forward" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 仅在simple模式下显示详细配置 -->
|
||||
<div class="camera-control mt-2" v-if="params.camera_control.type === 'simple'">
|
||||
<el-form-item label="水平移动">
|
||||
<el-slider
|
||||
v-model="params.camera_control.config.horizontal"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="垂直移动">
|
||||
<el-slider v-model="params.camera_control.config.vertical" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="左右旋转">
|
||||
<el-slider v-model="params.camera_control.config.pan" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上下旋转">
|
||||
<el-slider v-model="params.camera_control.config.tilt" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="横向翻转">
|
||||
<el-slider v-model="params.camera_control.config.roll" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="镜头缩放">
|
||||
<el-slider v-model="params.camera_control.config.zoom" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<div class="main-content task-list-inner">
|
||||
<!-- 任务类型选择 -->
|
||||
<div class="param-line">
|
||||
<el-tabs v-model="params.task_type" @tab-change="tabChange" class="title-tabs">
|
||||
<el-tab-pane label="文生视频" name="text2video">
|
||||
<div class="text">使用文字描述想要生成视频的内容</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="图生视频" name="image2video">
|
||||
<div class="text">
|
||||
以某张图片为底稿参考来创作视频,生成类似风格或类型视频,支持 PNG /JPG/JPEG
|
||||
格式图片;
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 生成操作区 -->
|
||||
<div class="generation-area">
|
||||
<div v-if="params.task_type === 'text2video'" class="text2video">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
type="textarea"
|
||||
maxlength="500"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词"
|
||||
/>
|
||||
<el-row class="text-info">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
@click="generatePrompt"
|
||||
:loading="isGenerating"
|
||||
size="small"
|
||||
color="#5865f2"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo"></i>
|
||||
生成专业视频提示词
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div v-else class="image2video">
|
||||
<div class="image-upload img-inline">
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
|
||||
<h4>起始帧</h4>
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadStartImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<img v-if="params.image" :src="params.image" class="preview" />
|
||||
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="btn-swap" v-if="params.image && params.image_tail">
|
||||
<i class="iconfont icon-exchange" @click="switchReverse"></i>
|
||||
</div>
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
<h4>结束帧</h4>
|
||||
<el-upload
|
||||
class="uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadEndImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<img v-if="params.image_tail" :src="params.image_tail" class="preview" />
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>提示词:</span>
|
||||
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="描述视频画面细节"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排除内容 -->
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>不希望出现的内容:(可选)</span>
|
||||
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="params.negative_prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入你不希望出现在视频上的内容"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<el-row class="text-info">
|
||||
<el-text type="primary"
|
||||
>每次生成视频消耗 <el-text type="warning">{{ powerCost }}算力;</el-text> </el-text
|
||||
>
|
||||
<el-text type="primary"
|
||||
>当前可用算力:<el-text type="warning">{{ availablePower }}</el-text></el-text
|
||||
>
|
||||
</el-row>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="submit-btn">
|
||||
<el-button type="primary" :dark="false" @click="generate" round>立即生成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表区域 -->
|
||||
<div class="video-list">
|
||||
<h2 class="text-xl p-3">你的作品</h2>
|
||||
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list" :key="item.id">
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video
|
||||
class="video"
|
||||
:src="item.video_url"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button
|
||||
class="play flex justify-center items-center"
|
||||
@click="previewVideo(item)"
|
||||
>
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image v-else-if="item.progress === 101" :src="item.cover_url" fit="cover" />
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="pb-2">
|
||||
<el-tag class="mr-1">{{ item.raw_data.task_type }}</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.model }}</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.duration }}秒</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.mode }}</el-tag>
|
||||
</div>
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>
|
||||
{{ substr(item.prompt, 1000) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
<div class="tools">
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
|
||||
<i class="iconfont icon-copy"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- <button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button> -->
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button
|
||||
class="btn btn-icon"
|
||||
@click="downloadVideo(item)"
|
||||
:disabled="item.downloading"
|
||||
>
|
||||
<i class="iconfont icon-download" v-if="!item.downloading"></i>
|
||||
<el-image
|
||||
src="/images/loading.gif"
|
||||
class="downloading"
|
||||
fit="cover"
|
||||
v-else
|
||||
/>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<button class="btn btn-icon" @click="removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-error" v-else>
|
||||
<el-button type="danger" @click="removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
background
|
||||
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<black-dialog
|
||||
v-model:show="previewVisible"
|
||||
title="视频预览"
|
||||
hide-footer
|
||||
@cancal="previewVisible = false"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
:src="currentVideo"
|
||||
controls
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
preload="auto"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr } from '@/utils/libs'
|
||||
import { CircleCloseFilled, InfoFilled, Plus } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
|
||||
const models = ref([
|
||||
{
|
||||
text: '可灵 1.6',
|
||||
value: 'kling-v1-6',
|
||||
},
|
||||
{
|
||||
text: '可灵 1.5',
|
||||
value: 'kling-v1-5',
|
||||
},
|
||||
{
|
||||
text: '可灵 1.0',
|
||||
value: 'kling-v1',
|
||||
},
|
||||
])
|
||||
// 参数设置
|
||||
const params = reactive({
|
||||
task_type: 'text2video',
|
||||
model: models.value[0].value,
|
||||
prompt: '',
|
||||
negative_prompt: '',
|
||||
cfg_scale: 0.7,
|
||||
mode: 'std',
|
||||
aspect_ratio: '16:9',
|
||||
duration: '5',
|
||||
camera_control: {
|
||||
type: '',
|
||||
config: {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
pan: 0,
|
||||
tilt: 0,
|
||||
roll: 0,
|
||||
zoom: 0,
|
||||
},
|
||||
},
|
||||
image: '',
|
||||
image_tail: '',
|
||||
})
|
||||
const rates = [
|
||||
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.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 changeRate = (item) => {
|
||||
params.aspect_ratio = item.value
|
||||
}
|
||||
|
||||
const generating = ref(false)
|
||||
const isGenerating = ref(false)
|
||||
const powerCost = ref(10)
|
||||
const availablePower = ref(100)
|
||||
const taskFilter = ref('all')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const pullHandler = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
const currentVideo = ref('')
|
||||
const showCameraControl = ref(false)
|
||||
const keLingPowers = ref({})
|
||||
const isLogin = ref(false)
|
||||
// 动态更新模型消耗的算力
|
||||
const updateModelPower = () => {
|
||||
showCameraControl.value = params.model === 'kling-v1-5' && params.mode === 'pro'
|
||||
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {}
|
||||
}
|
||||
|
||||
// tab切换
|
||||
const tabChange = (tab) => {
|
||||
params.task_type = tab
|
||||
}
|
||||
|
||||
const uploadStartImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
try {
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
params.image = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
closeLoading()
|
||||
} catch (e) {
|
||||
showMessageError('上传失败: ' + e.message)
|
||||
closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
//移除图片
|
||||
const removeImage = (type) => {
|
||||
if (type === 'start') {
|
||||
params.image = ''
|
||||
} else if (type === 'end') {
|
||||
params.image_tail = ''
|
||||
}
|
||||
}
|
||||
|
||||
//图片交换方法
|
||||
const switchReverse = () => {
|
||||
;[params.image, params.image_tail] = [params.image_tail, params.image]
|
||||
}
|
||||
const uploadEndImage = async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
try {
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
params.image_tail = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch (e) {
|
||||
showMessageError('上传失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const generatePrompt = async () => {
|
||||
if (isGenerating.value) return
|
||||
if (!params.prompt) {
|
||||
return showMessageError('请输入视频描述')
|
||||
}
|
||||
isGenerating.value = true
|
||||
try {
|
||||
const res = await httpPost('/api/prompt/video', { prompt: params.prompt })
|
||||
params.prompt = res.data
|
||||
} catch (e) {
|
||||
showMessageError('生成失败: ' + e.message)
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const generate = async () => {
|
||||
//增加防抖
|
||||
if (generating.value) return
|
||||
if (!params.prompt?.trim()) {
|
||||
return ElMessage.error('请输入视频描述')
|
||||
}
|
||||
// 提示词长度不能超过 500
|
||||
if (params.prompt.length > 500) {
|
||||
return ElMessage.error('视频描述不能超过 500 个字符')
|
||||
}
|
||||
if (params.task_type === 'image2video' && !params.image) {
|
||||
return ElMessage.error('请上传起始帧图片')
|
||||
}
|
||||
generating.value = true
|
||||
// 处理图片链接
|
||||
if (params.image) {
|
||||
params.image = replaceImg(params.image)
|
||||
}
|
||||
if (params.image_tail) {
|
||||
params.image_tail = replaceImg(params.image_tail)
|
||||
}
|
||||
try {
|
||||
await httpPost('/api/video/keling/create', params)
|
||||
showMessageOK('任务创建成功')
|
||||
// 新增重置
|
||||
page.value = 1
|
||||
list.value.unshift({
|
||||
progress: 0,
|
||||
prompt: params.prompt,
|
||||
raw_data: {
|
||||
task_type: params.task_type,
|
||||
model: params.model,
|
||||
duration: params.duration,
|
||||
mode: params.mode,
|
||||
},
|
||||
})
|
||||
taskPulling.value = true
|
||||
} catch (e) {
|
||||
showMessageError('创建失败: ' + e.message)
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page
|
||||
}
|
||||
|
||||
httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: 'keling',
|
||||
task_type: taskFilter.value === 'all' ? '' : taskFilter.value,
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true
|
||||
}
|
||||
items.push({
|
||||
...v,
|
||||
downloading: false,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const previewVideo = (task) => {
|
||||
currentVideo.value = task.video_url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
const downloadVideo = async (task) => {
|
||||
try {
|
||||
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`)
|
||||
const blob = new Blob([res.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = `video_${task.id}.mp4`
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (e) {
|
||||
showMessageError('下载失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet('/api/video/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData(page.value)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((u) => {
|
||||
isLogin.value = true
|
||||
availablePower.value = u.power
|
||||
fetchData(1)
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(page.value)
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
|
||||
getSystemInfo().then((res) => {
|
||||
keLingPowers.value = res.data.keling_powers
|
||||
updateModelPower()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '../assets/css/keling.styl'
|
||||
</style>
|
||||
@@ -1,387 +0,0 @@
|
||||
<template>
|
||||
<div class="page-luma">
|
||||
<div class="prompt-box">
|
||||
<div class="images">
|
||||
<template v-for="(img, index) in images" :key="img">
|
||||
<div class="item">
|
||||
<el-image :src="replaceImg(img)" fit="cover" />
|
||||
<el-icon @click="remove(img)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
<div class="btn-swap" v-if="images.length === 2 && index === 0">
|
||||
<i class="iconfont icon-exchange" @click="switchReverse"></i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="prompt-container">
|
||||
<div class="input-container">
|
||||
<div class="upload-icon" v-if="images.length < 2">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="upload"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<i class="iconfont icon-image"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
<textarea
|
||||
class="prompt-input"
|
||||
:rows="row"
|
||||
v-model="formData.prompt"
|
||||
maxlength="2000"
|
||||
placeholder="请输入提示词或者上传图片"
|
||||
autofocus
|
||||
>
|
||||
</textarea>
|
||||
<div class="send-icon" @click="create">
|
||||
<i class="iconfont icon-send"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="params">
|
||||
<div class="item-group">
|
||||
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2">
|
||||
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
|
||||
<span>生成AI视频提示词</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="item-group">
|
||||
<span class="label">循环参考图</span>
|
||||
<el-switch v-model="formData.loop" size="small" />
|
||||
</div>
|
||||
<div class="item-group">
|
||||
<span class="label">提示词优化</span>
|
||||
<el-switch v-model="formData.expand_prompt" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-container
|
||||
class="video-container"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(100,100,100,0.3)"
|
||||
>
|
||||
<h2 class="h-title text-2xl mb-5 mt-2">你的作品</h2>
|
||||
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list" :key="item.id">
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video
|
||||
class="video"
|
||||
:src="replaceImg(item.video_url)"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button class="play flex justify-center items-center" @click="play(item)">
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress === 101" />
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>{{ item.prompt }}</div>
|
||||
</div>
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
<div class="tools">
|
||||
<!-- <button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button> -->
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
|
||||
<i class="iconfont icon-copy"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button class="btn btn-icon" @click="download(item)" :disabled="item.downloading">
|
||||
<i class="iconfont icon-download" v-if="!item.downloading"></i>
|
||||
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<button class="btn btn-icon" @click="removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-error" v-else>
|
||||
<el-button type="danger" @click="removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
background
|
||||
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData(page)"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</el-container>
|
||||
<black-dialog
|
||||
v-model:show="showDialog"
|
||||
title="预览视频"
|
||||
hide-footer
|
||||
@cancal="showDialog = false"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:src="currentVideoUrl"
|
||||
preload="auto"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
v-show="showDialog"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
import { CircleCloseFilled } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
const row = ref(1)
|
||||
const images = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
prompt: '',
|
||||
expand_prompt: false,
|
||||
loop: false,
|
||||
first_frame_img: '',
|
||||
end_frame_img: '',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const clipboard = ref(null)
|
||||
const pullHandler = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
fetchData(1)
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(1)
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
})
|
||||
|
||||
const download = (item) => {
|
||||
const url = replaceImg(item.video_url)
|
||||
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
item.downloading = true
|
||||
httpDownload(downloadURL)
|
||||
.then((response) => {
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
})
|
||||
.catch(() => {
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
})
|
||||
}
|
||||
|
||||
const play = (item) => {
|
||||
currentVideoUrl.value = replaceImg(item.video_url)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet('/api/video/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const publishJob = (item) => {
|
||||
httpGet('/api/video/publish', { id: item.id, publish: item.publish })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const upload = (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file, file.name)
|
||||
showLoading('正在上传文件...')
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
images.value.push(res.data.url)
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const remove = (img) => {
|
||||
images.value = images.value.filter((item) => item !== img)
|
||||
}
|
||||
|
||||
const switchReverse = () => {
|
||||
images.value = images.value.reverse()
|
||||
}
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page
|
||||
}
|
||||
httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: 'luma',
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true
|
||||
}
|
||||
items.push(v)
|
||||
}
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const create = () => {
|
||||
const len = images.value.length
|
||||
if (len) {
|
||||
formData.first_frame_img = replaceImg(images.value[0])
|
||||
if (len === 2) {
|
||||
formData.end_frame_img = replaceImg(images.value[1])
|
||||
}
|
||||
}
|
||||
|
||||
httpPost('/api/video/luma/create', formData)
|
||||
.then(() => {
|
||||
fetchData(1)
|
||||
taskPulling.value = true
|
||||
showMessageOK('创建任务成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('创建任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const generatePrompt = () => {
|
||||
if (formData.prompt === '') {
|
||||
return showMessageError('请输入原始提示词')
|
||||
}
|
||||
showLoading('正在生成视频脚本...')
|
||||
httpPost('/api/prompt/video', { prompt: formData.prompt })
|
||||
.then((res) => {
|
||||
formData.prompt = res.data
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('生成提示词失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/luma.styl"
|
||||
</style>
|
||||
@@ -7,27 +7,37 @@
|
||||
:element-loading-text="loadingText"
|
||||
>
|
||||
<div class="inner">
|
||||
<div class="user-profile">
|
||||
<user-profile :key="profileKey" />
|
||||
|
||||
<el-row class="user-opt" :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="showBindEmailDialog = true">绑定邮箱</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="showBindMobileDialog = true">绑定手机</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="showThirdLoginDialog = true">第三方登录</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="showPasswordDialog = true">修改密码</el-button>
|
||||
<el-card class="profile-card">
|
||||
<el-row class="user-opt" :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-button class="profile-btn email" @click="showBindEmailDialog = true">
|
||||
<i class="iconfont icon-email"></i> 绑定邮箱
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-button type="primary" @click="showRedeemVerifyDialog = true">卡密兑换 </el-button>
|
||||
<el-button class="profile-btn mobile" @click="showBindMobileDialog = true">
|
||||
<i class="iconfont icon-mobile"></i> 绑定手机
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-button class="profile-btn third" @click="showThirdLoginDialog = true">
|
||||
<i class="iconfont icon-login"></i> 第三方登录
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-button class="profile-btn password" @click="showPasswordDialog = true">
|
||||
<i class="iconfont icon-password"></i> 修改密码
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-divider />
|
||||
<el-col :span="24">
|
||||
<el-button class="profile-btn redeem" @click="showRedeemVerifyDialog = true">
|
||||
<i class="iconfont icon-redeem"></i> 卡密兑换
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
<div class="profile-bg"></div>
|
||||
|
||||
<div class="product-box">
|
||||
<div class="info" v-if="orderPayInfoText !== ''">
|
||||
@@ -158,7 +168,6 @@ import PasswordDialog from '@/components/PasswordDialog.vue'
|
||||
import RedeemVerify from '@/components/RedeemVerify.vue'
|
||||
import ThirdLogin from '@/components/ThirdLogin.vue'
|
||||
import UserOrder from '@/components/UserOrder.vue'
|
||||
import UserProfile from '@/components/UserProfile.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
@@ -185,7 +194,6 @@ const orderPayInfoText = ref('')
|
||||
const payWays = ref([])
|
||||
const vipInfoText = ref('')
|
||||
const store = useSharedStore()
|
||||
const profileKey = ref(0)
|
||||
const userOrderKey = ref(0)
|
||||
const showDialog = ref(false)
|
||||
const qrImg = ref('')
|
||||
@@ -276,17 +284,13 @@ const pay = (product, payWay) => {
|
||||
})
|
||||
}
|
||||
|
||||
const redeemCallback = (success) => {
|
||||
const redeemCallback = () => {
|
||||
showRedeemVerifyDialog.value = false
|
||||
if (success) {
|
||||
profileKey.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
const payCallback = (success) => {
|
||||
showDialog.value = false
|
||||
if (success) {
|
||||
profileKey.value += 1
|
||||
userOrderKey.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
645
web/src/views/Video.vue
Normal file
645
web/src/views/Video.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<template>
|
||||
<div class="page-video">
|
||||
<!-- 左侧参数设置面板 -->
|
||||
<div class="params-panel">
|
||||
<!-- 视频类型切换标签页 -->
|
||||
<el-tabs
|
||||
v-model="store.activeVideoType"
|
||||
@tab-change="store.switchVideoType"
|
||||
class="video-type-tabs"
|
||||
>
|
||||
<!-- Luma 视频参数 -->
|
||||
<el-tab-pane label="Luma视频" name="luma">
|
||||
<div class="params-container">
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.lumaParams.prompt"
|
||||
type="textarea"
|
||||
maxlength="2000"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入视频提示词,用逗号分割,您也可以点击下面的提示词助手生成视频提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提示词生成按钮 -->
|
||||
<div class="param-line pt">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
@click="store.generatePrompt"
|
||||
:loading="store.isGenerating"
|
||||
size="small"
|
||||
color="#5865f2"
|
||||
style="width: 100%"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo"></i>
|
||||
生成AI视频提示词
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 图片辅助生成开关 -->
|
||||
<div class="param-line pt">
|
||||
<div class="image-mode-toggle">
|
||||
<span class="label">使用图片辅助生成</span>
|
||||
<el-switch
|
||||
v-model="store.lumaUseImageMode"
|
||||
@change="store.toggleLumaImageMode"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片上传区域(可折叠) -->
|
||||
<div v-if="store.lumaUseImageMode" class="img-inline">
|
||||
<div class="img-uploader video-img-box mr-2">
|
||||
<el-icon
|
||||
v-if="store.lumaParams.image"
|
||||
@click="store.removeLumaImage('start')"
|
||||
class="removeimg"
|
||||
>
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="store.uploadLumaStartImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<el-image
|
||||
v-if="store.lumaParams.image"
|
||||
:src="store.lumaParams.image"
|
||||
fit="cover"
|
||||
/>
|
||||
<div class="flex flex-col" v-else>
|
||||
<el-icon class="mb-1 text-base"><Plus /></el-icon>
|
||||
<span>起始帧</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center h-[120px] cursor-pointer"
|
||||
v-if="store.lumaParams.image && store.lumaParams.image_tail"
|
||||
>
|
||||
<el-tooltip content="交换图片" placement="top">
|
||||
<i class="iconfont icon-exchange" @click="store.switchLumaImages"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="img-uploader video-img-box ml-2">
|
||||
<el-icon
|
||||
v-if="store.lumaParams.image_tail"
|
||||
@click="store.removeLumaImage('end')"
|
||||
class="removeimg"
|
||||
>
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="store.uploadLumaEndImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<el-image
|
||||
v-if="store.lumaParams.image_tail"
|
||||
:src="store.lumaParams.image_tail"
|
||||
fit="cover"
|
||||
/>
|
||||
<div class="flex flex-col" v-else>
|
||||
<el-icon class="mb-1 text-base"><Plus /></el-icon>
|
||||
<span>结束帧</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Luma 特有参数设置 -->
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">循环参考图</span>
|
||||
<el-switch v-model="store.lumaParams.loop" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">提示词优化</span>
|
||||
<el-switch v-model="store.lumaParams.expand_prompt" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<el-row class="text-info">
|
||||
<el-text type="primary"
|
||||
>当前可用算力:<el-text type="warning">{{ store.availablePower }}</el-text></el-text
|
||||
>
|
||||
</el-row>
|
||||
<!-- 生成按钮 -->
|
||||
<div class="submit-btn">
|
||||
<el-button type="primary" :dark="false" @click="store.createLumaVideo" round>
|
||||
立即生成 ({{ store.lumaPowerCost }}<i class="iconfont icon-vip2"></i>)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- KeLing 视频参数 -->
|
||||
<el-tab-pane label="可灵视频" name="keling">
|
||||
<div class="params-container">
|
||||
<el-form :model="store.kelingParams" label-width="80px" label-position="left">
|
||||
<!-- 画面比例 -->
|
||||
<div class="param-line">
|
||||
<div class="param-line pt">
|
||||
<span>画面比例:</span>
|
||||
<el-tooltip content="生成画面的尺寸比例" placement="right">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="item in store.rates" :key="item.value">
|
||||
<div
|
||||
class="flex-col items-center"
|
||||
:class="
|
||||
item.value === store.kelingParams.aspect_ratio
|
||||
? 'grid-content active'
|
||||
: 'grid-content'
|
||||
"
|
||||
@click="store.changeRate(item)"
|
||||
>
|
||||
<el-image class="icon proportion" :src="item.img" fit="cover"></el-image>
|
||||
<div class="texts">{{ item.text }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型选择 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="模型选择">
|
||||
<el-select
|
||||
v-model="store.kelingParams.model"
|
||||
placeholder="请选择模型"
|
||||
@change="store.updateModelPower"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in store.models"
|
||||
:key="item.value"
|
||||
:label="item.text"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 视频时长 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="视频时长">
|
||||
<el-select
|
||||
v-model="store.kelingParams.duration"
|
||||
placeholder="请选择时长"
|
||||
@change="store.updateModelPower"
|
||||
>
|
||||
<el-option label="5秒" value="5" />
|
||||
<el-option label="10秒" value="10" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 生成模式 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="生成模式">
|
||||
<el-select
|
||||
v-model="store.kelingParams.mode"
|
||||
placeholder="请选择模式"
|
||||
@change="store.updateModelPower"
|
||||
>
|
||||
<el-option label="标准模式" value="std" />
|
||||
<el-option label="专业模式" value="pro" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 创意程度 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="创意程度">
|
||||
<el-slider v-model="store.kelingParams.cfg_scale" :min="0" :max="1" :step="0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 运镜控制 -->
|
||||
<div class="param-line" v-if="store.showCameraControl">
|
||||
<div class="param-line pt">
|
||||
<span>运镜控制:</span>
|
||||
<el-tooltip content="生成画面的运镜效果,仅 1.5的高级模式可用" placement="right">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-select
|
||||
v-model="store.kelingParams.camera_control.type"
|
||||
placeholder="请选择运镜类型"
|
||||
>
|
||||
<el-option label="请选择" value="" />
|
||||
<el-option label="简单运镜" value="simple" />
|
||||
<el-option label="下移拉远" value="down_back" />
|
||||
<el-option label="推进上移" value="forward_up" />
|
||||
<el-option label="右旋推进" value="right_turn_forward" />
|
||||
<el-option label="左旋推进" value="left_turn_forward" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 仅在simple模式下显示详细配置 -->
|
||||
<div
|
||||
class="camera-control mt-2"
|
||||
v-if="store.kelingParams.camera_control.type === 'simple'"
|
||||
>
|
||||
<el-form-item label="水平移动">
|
||||
<el-slider
|
||||
v-model="store.kelingParams.camera_control.config.horizontal"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="垂直移动">
|
||||
<el-slider
|
||||
v-model="store.kelingParams.camera_control.config.vertical"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="左右旋转">
|
||||
<el-slider
|
||||
v-model="store.kelingParams.camera_control.config.pan"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上下旋转">
|
||||
<el-slider
|
||||
v-model="store.kelingParams.camera_control.config.tilt"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="横向翻转">
|
||||
<el-slider
|
||||
v-model="store.kelingParams.camera_control.config.roll"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="镜头缩放">
|
||||
<el-slider
|
||||
v-model="store.kelingParams.camera_control.config.zoom"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 图片辅助生成开关 -->
|
||||
<div class="param-line pt">
|
||||
<div class="image-mode-toggle">
|
||||
<span class="label">使用图片辅助生成</span>
|
||||
<el-switch
|
||||
v-model="store.kelingUseImageMode"
|
||||
@change="store.toggleKelingImageMode"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片上传区域(可折叠) -->
|
||||
<div v-if="store.kelingUseImageMode" class="img-inline">
|
||||
<div class="img-uploader video-img-box mr-2">
|
||||
<el-icon
|
||||
v-if="store.kelingParams.image"
|
||||
@click="store.removeKelingImage('start')"
|
||||
class="removeimg"
|
||||
>
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="store.uploadKelingStartImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<el-image
|
||||
v-if="store.kelingParams.image"
|
||||
:src="store.kelingParams.image"
|
||||
fit="cover"
|
||||
/>
|
||||
<div class="flex flex-col" v-else>
|
||||
<el-icon class="mb-1 text-base"><Plus /></el-icon>
|
||||
<span>起始帧</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center h-[120px] cursor-pointer"
|
||||
v-if="store.kelingParams.image && store.kelingParams.image_tail"
|
||||
>
|
||||
<el-tooltip content="交换图片" placement="top">
|
||||
<i class="iconfont icon-exchange" @click="store.switchKelingImages"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="img-uploader video-img-box ml-2">
|
||||
<el-icon
|
||||
v-if="store.kelingParams.image_tail"
|
||||
@click="store.removeKelingImage('end')"
|
||||
class="removeimg"
|
||||
>
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="store.uploadKelingEndImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<el-image
|
||||
v-if="store.kelingParams.image_tail"
|
||||
:src="store.kelingParams.image_tail"
|
||||
fit="cover"
|
||||
/>
|
||||
<div class="flex flex-col" v-else>
|
||||
<el-icon class="mb-1 text-base"><Plus /></el-icon>
|
||||
<span>结束帧</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词输入 -->
|
||||
<div class="param-line pt">
|
||||
<span>提示词:</span>
|
||||
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="store.kelingParams.prompt"
|
||||
type="textarea"
|
||||
maxlength="500"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
:placeholder="
|
||||
store.kelingUseImageMode
|
||||
? '描述视频画面细节'
|
||||
: '请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提示词生成按钮 -->
|
||||
<div class="param-line pt">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
@click="store.generatePrompt"
|
||||
:loading="store.isGenerating"
|
||||
size="small"
|
||||
color="#5865f2"
|
||||
style="width: 100%"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo"></i>
|
||||
生成专业视频提示词
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 排除内容 -->
|
||||
<div class="param-line pt">
|
||||
<span>不希望出现的内容:(可选)</span>
|
||||
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="store.kelingParams.negative_prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入你不希望出现在视频上的内容"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<el-row class="text-info">
|
||||
<el-text type="primary"
|
||||
>当前可用算力:<el-text type="warning">{{ store.availablePower }}</el-text></el-text
|
||||
>
|
||||
</el-row>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="submit-btn">
|
||||
<el-button
|
||||
type="primary"
|
||||
:dark="false"
|
||||
@click="store.createKelingVideo"
|
||||
round
|
||||
:loading="store.generating"
|
||||
>
|
||||
立即生成 ({{ store.kelingPowerCost }}<i class="iconfont icon-vip2"></i>)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 右侧任务列表 -->
|
||||
<div
|
||||
class="main-content"
|
||||
v-loading="store.loading"
|
||||
element-loading-background="rgba(100,100,100,0.3)"
|
||||
>
|
||||
<div class="works-header">
|
||||
<h2 class="h-title text-2xl">你的作品</h2>
|
||||
<div class="filter-buttons">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'all' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('all')"
|
||||
size="small"
|
||||
>
|
||||
全部
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'luma' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('luma')"
|
||||
size="small"
|
||||
>
|
||||
Luma
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'keling' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('keling')"
|
||||
size="small"
|
||||
>
|
||||
可灵
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-list">
|
||||
<div class="list-box" v-if="!store.noData">
|
||||
<div v-for="item in store.currentList" :key="item.id">
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video
|
||||
class="video"
|
||||
:src="store.replaceImg(item.video_url)"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button
|
||||
class="play flex justify-center items-center"
|
||||
@click="store.playVideo(item)"
|
||||
>
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image
|
||||
:src="item.cover_url"
|
||||
class="border rounded-lg"
|
||||
fit="cover"
|
||||
v-else-if="item.progress === 101"
|
||||
/>
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="pb-2" v-if="item.raw_data">
|
||||
<el-tag class="mr-1">{{
|
||||
item.raw_data.task_type || store.activeVideoType
|
||||
}}</el-tag>
|
||||
<el-tag class="mr-1" v-if="item.raw_data.model">{{ item.raw_data.model }}</el-tag>
|
||||
<el-tag class="mr-1" v-if="item.raw_data.duration"
|
||||
>{{ item.raw_data.duration }}秒</el-tag
|
||||
>
|
||||
<el-tag class="mr-1" v-if="item.raw_data.mode">{{ item.raw_data.mode }}</el-tag>
|
||||
</div>
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>
|
||||
{{ store.substr(item.prompt, 1000) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
<div class="tools">
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
|
||||
<i class="iconfont icon-copy"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button
|
||||
class="btn btn-icon"
|
||||
@click="store.downloadVideo(item)"
|
||||
:disabled="item.downloading"
|
||||
>
|
||||
<i class="iconfont icon-download" v-if="!item.downloading"></i>
|
||||
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<button class="btn btn-icon" @click="store.removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-error" v-else>
|
||||
<el-button type="danger" @click="store.removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="store.nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-if="store.total > store.pageSize"
|
||||
background
|
||||
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
:current-page="store.page"
|
||||
:page-size="store.pageSize"
|
||||
@current-change="store.fetchData"
|
||||
:total="store.total"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<black-dialog
|
||||
:show="store.showDialog"
|
||||
title="预览视频"
|
||||
hide-footer
|
||||
@cancal="store.showDialog = false"
|
||||
@update:show="store.showDialog = $event"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:src="store.currentVideoUrl"
|
||||
preload="auto"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
v-show="store.showDialog"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { useVideoStore } from '@/store/video'
|
||||
import { CircleCloseFilled, InfoFilled, Plus } from '@element-plus/icons-vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const store = useVideoStore()
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/video.styl"
|
||||
</style>
|
||||
@@ -56,7 +56,7 @@
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
开放注册
|
||||
菜单图标
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="可以填写 iconfont 图标名称也可以自己上传图片"
|
||||
|
||||
@@ -169,10 +169,10 @@
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
默认翻译模型
|
||||
系统辅助AI模型
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="选择一个默认模型来翻译提示词"
|
||||
content="用来辅助用户生成提示词,翻译的AI模型,默认使用 gpt-4o-mini"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
@@ -183,9 +183,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<el-select
|
||||
v-model.number="system['translate_model_id']"
|
||||
v-model.number="system['assistant_model_id']"
|
||||
:filterable="true"
|
||||
placeholder="选择一个默认模型来翻译提示词"
|
||||
placeholder="选择一个系统辅助AI模型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
|
||||
526
web/src/views/admin/jimeng/JimengJobs.vue
Normal file
526
web/src/views/admin/jimeng/JimengJobs.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="4">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stats.totalTasks }}</div>
|
||||
<div class="stat-label">总任务数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number !text-blue-500">{{ stats.pendingTasks }}</div>
|
||||
<div class="stat-label">排队中</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number warning">{{ stats.processingTasks }}</div>
|
||||
<div class="stat-label">处理中</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number success">{{ stats.completedTasks }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number danger">{{ stats.failedTasks }}</div>
|
||||
<div class="stat-label">失败</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<el-form :model="queryForm" ref="queryFormRef" :inline="true" label-width="80px">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input
|
||||
v-model="queryForm.user_id"
|
||||
placeholder="请输入用户ID"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型">
|
||||
<el-select
|
||||
v-model="queryForm.type"
|
||||
placeholder="请选择任务类型"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option label="文生图" value="text_to_image" />
|
||||
<el-option label="图生图" value="image_to_image" />
|
||||
<el-option label="图像编辑" value="image_edit" />
|
||||
<el-option label="图像特效" value="image_effects" />
|
||||
<el-option label="文生视频" value="text_to_video" />
|
||||
<el-option label="图生视频" value="image_to_video" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务状态">
|
||||
<el-select
|
||||
v-model="queryForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option label="等待中" value="in_queue" />
|
||||
<el-option label="处理中" value="generating" />
|
||||
<el-option label="已完成" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :loading="loading">
|
||||
<i class="iconfont icon-search mr-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleBatchDelete" :disabled="!multipleSelection.length">
|
||||
<i class="iconfont icon-remove mr-1" />
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-card class="table-card">
|
||||
<el-table
|
||||
:data="taskList"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
border
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||
<el-table-column prop="type" label="任务类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ getTaskTypeName(scope.row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="prompt" label="提示词" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusColor(scope.row.status)" size="small">
|
||||
{{ getStatusName(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress" label="进度" width="100">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.progress" :stroke-width="4" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="power" label="算力" width="80" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" text @click="handleViewDetail(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 任务详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialog.visible"
|
||||
:title="`任务详情 - ${detailDialog.data.id}`"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="detail-content" v-if="detailDialog.data">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="任务ID">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{
|
||||
detailDialog.data.user_id
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务类型">{{
|
||||
getTaskTypeName(detailDialog.data.type)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusColor(detailDialog.data.status)">
|
||||
{{ getStatusName(detailDialog.data.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="进度"
|
||||
>{{ detailDialog.data.progress }}%</el-descriptions-item
|
||||
>
|
||||
<el-descriptions-item label="算力消耗">{{
|
||||
detailDialog.data.power
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{
|
||||
formatDateTime(detailDialog.data.created_at)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{
|
||||
formatDateTime(detailDialog.data.updated_at)
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4 class="text-base pt-2 font-bold">提示词</h4>
|
||||
<div class="prompt-content">{{ detailDialog.data.prompt || '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.task_params">
|
||||
<h4>任务参数</h4>
|
||||
<el-input
|
||||
v-model="detailDialog.data.task_params"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
readonly
|
||||
class="params-content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.err_msg">
|
||||
<h4>错误信息</h4>
|
||||
<el-alert :title="detailDialog.data.err_msg" type="error" :closable="false" />
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.img_url || detailDialog.data.video_url">
|
||||
<h4>生成结果</h4>
|
||||
<div class="result-content">
|
||||
<div v-if="detailDialog.data.img_url" class="result-item">
|
||||
<label>图片:</label>
|
||||
<el-image
|
||||
:src="detailDialog.data.img_url"
|
||||
:preview-src-list="[detailDialog.data.img_url]"
|
||||
fit="cover"
|
||||
style="width: 100px; height: 100px; border-radius: 4px"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="detailDialog.data.video_url" class="result-item">
|
||||
<label>视频:</label>
|
||||
<video
|
||||
:src="detailDialog.data.video_url"
|
||||
controls
|
||||
style="width: 200px; height: 150px; border-radius: 4px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.raw_data">
|
||||
<h4>原始响应数据</h4>
|
||||
<el-input
|
||||
v-model="formattedRawData"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
readonly
|
||||
class="raw-data-content"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { formatDateTime } from '@/utils/libs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
// 查询表单
|
||||
const queryForm = reactive({
|
||||
user_id: '',
|
||||
type: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 数据
|
||||
const taskList = ref([])
|
||||
const loading = ref(false)
|
||||
const multipleSelection = ref([])
|
||||
const queryFormRef = ref(null)
|
||||
|
||||
// 统计信息
|
||||
const stats = reactive({
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
processingTasks: 0,
|
||||
failedTasks: 0,
|
||||
})
|
||||
|
||||
// 详情对话框
|
||||
const detailDialog = reactive({
|
||||
visible: false,
|
||||
data: {},
|
||||
})
|
||||
|
||||
// 格式化原始数据
|
||||
const formattedRawData = computed(() => {
|
||||
if (!detailDialog.data.raw_data) return ''
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(detailDialog.data.raw_data), null, 2)
|
||||
} catch (error) {
|
||||
return detailDialog.data.raw_data
|
||||
}
|
||||
})
|
||||
|
||||
// 获取任务类型名称
|
||||
const getTaskTypeName = (type) => {
|
||||
const typeMap = {
|
||||
text_to_image: '文生图',
|
||||
image_to_image: '图生图',
|
||||
image_edit: '图像编辑',
|
||||
image_effects: '图像特效',
|
||||
text_to_video: '文生视频',
|
||||
image_to_video: '图生视频',
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = {
|
||||
in_queue: '等待中',
|
||||
generating: '处理中',
|
||||
success: '已完成',
|
||||
failed: '失败',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
in_queue: '',
|
||||
generating: 'warning',
|
||||
success: 'success',
|
||||
failed: 'danger',
|
||||
}
|
||||
return colorMap[status] || ''
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
const getTaskList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...queryForm,
|
||||
}
|
||||
|
||||
const response = await httpGet('/api/admin/jimeng/jobs', params)
|
||||
taskList.value = response.data.jobs || []
|
||||
pagination.total = response.data.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error('获取任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const response = await httpGet('/api/admin/jimeng/stats')
|
||||
Object.assign(stats, response.data)
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
pagination.page = 1
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
multipleSelection.value = selection
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (row) => {
|
||||
try {
|
||||
const response = await httpGet(`/api/admin/jimeng/jobs/${row.id}`)
|
||||
detailDialog.data = response.data
|
||||
detailDialog.visible = true
|
||||
} catch (error) {
|
||||
ElMessage.error('获取任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (!multipleSelection.value.length) {
|
||||
ElMessage.warning('请选择要删除的任务')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
const jobIds = multipleSelection.value.map((item) => item.id)
|
||||
await httpPost('/api/admin/jimeng/jobs/remove', { job_ids: jobIds })
|
||||
ElMessage.success('批量删除成功')
|
||||
getTaskList()
|
||||
getStats()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
pagination.page = 1
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getTaskList()
|
||||
getStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.app-container
|
||||
padding 20px
|
||||
|
||||
.el-form-item
|
||||
margin-bottom 0
|
||||
|
||||
.page-header
|
||||
margin-bottom 20px
|
||||
|
||||
h2
|
||||
margin 0 0 8px 0
|
||||
color #303133
|
||||
|
||||
p
|
||||
margin 0
|
||||
color #606266
|
||||
font-size 14px
|
||||
|
||||
.filter-card
|
||||
margin-bottom 20px
|
||||
|
||||
.stats-row
|
||||
margin-bottom 20px
|
||||
|
||||
.stat-card
|
||||
.stat-item
|
||||
text-align center
|
||||
padding 20px
|
||||
|
||||
.stat-number
|
||||
font-size 28px
|
||||
font-weight bold
|
||||
color #303133
|
||||
margin-bottom 8px
|
||||
|
||||
&.success
|
||||
color #67c23a
|
||||
|
||||
&.warning
|
||||
color #e6a23c
|
||||
|
||||
&.danger
|
||||
color #f56c6c
|
||||
|
||||
.stat-label
|
||||
font-size 14px
|
||||
color #909399
|
||||
|
||||
.table-card
|
||||
.pagination-container
|
||||
margin-top 20px
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.detail-content
|
||||
.detail-section
|
||||
margin-bottom 20px
|
||||
|
||||
h4
|
||||
margin 0 0 10px 0
|
||||
color #303133
|
||||
font-size 16px
|
||||
|
||||
.prompt-content
|
||||
background #f5f7fa
|
||||
padding 12px
|
||||
border-radius 4px
|
||||
color #606266
|
||||
line-height 1.6
|
||||
|
||||
.params-content, .raw-data-content
|
||||
font-family monospace
|
||||
|
||||
.result-content
|
||||
.result-item
|
||||
margin-bottom 10px
|
||||
display flex
|
||||
align-items center
|
||||
gap 10px
|
||||
|
||||
label
|
||||
font-weight bold
|
||||
color #303133
|
||||
min-width 50px
|
||||
</style>
|
||||
281
web/src/views/admin/jimeng/JimengSetting.vue
Normal file
281
web/src/views/admin/jimeng/JimengSetting.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="system-config form" v-loading="loading">
|
||||
<div class="container">
|
||||
<el-form
|
||||
:model="jimengConfig"
|
||||
label-width="150px"
|
||||
label-position="right"
|
||||
ref="configFormRef"
|
||||
:rules="rules"
|
||||
class="py-3 px-5"
|
||||
>
|
||||
<!-- 秘钥配置分组 -->
|
||||
<div class="mb-3">
|
||||
<h3 class="mb-2">秘钥配置</h3>
|
||||
<el-form-item label="AccessKey" prop="access_key">
|
||||
<el-input
|
||||
v-model="jimengConfig.access_key"
|
||||
placeholder="请输入即梦AI的AccessKey"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SecretKey" prop="secret_key">
|
||||
<el-input
|
||||
v-model="jimengConfig.secret_key"
|
||||
placeholder="请输入即梦AI的SecretKey"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-divider />
|
||||
<!-- 算力配置分组 -->
|
||||
<div class="mb-3">
|
||||
<h3 class="mb-3">算力配置</h3>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
文生图算力
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户使用文生图功能时消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.text_to_image"
|
||||
:min="1"
|
||||
placeholder="请输入文生图算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
图生图算力
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户使用图生图功能时消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_to_image"
|
||||
:min="1"
|
||||
placeholder="请输入图生图算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
图片编辑算力
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户使用图片编辑功能时消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_edit"
|
||||
:min="1"
|
||||
placeholder="请输入图片编辑算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
图片特效算力
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户使用图片特效功能时消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_effects"
|
||||
:min="1"
|
||||
placeholder="请输入图片特效算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
文生视频算力
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户使用文生视频功能时消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.text_to_video"
|
||||
:min="1"
|
||||
placeholder="请输入文生视频算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
图生视频算力
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户使用图生视频功能时消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_to_video"
|
||||
:min="1"
|
||||
placeholder="请输入图生视频算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div style="padding: 10px">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveConfig" :loading="saving">保存配置</el-button>
|
||||
<el-button @click="resetConfig">重置</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const jimengConfig = ref({
|
||||
access_key: '',
|
||||
secret_key: '',
|
||||
power: {
|
||||
text_to_image: 10,
|
||||
image_to_image: 15,
|
||||
image_edit: 20,
|
||||
image_effects: 25,
|
||||
text_to_video: 30,
|
||||
image_to_video: 35,
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const testing = ref(false)
|
||||
const configFormRef = ref()
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
access_key: [{ required: true, message: '请输入AccessKey', trigger: 'blur' }],
|
||||
secret_key: [{ required: true, message: '请输入SecretKey', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/admin/jimeng/config')
|
||||
jimengConfig.value = res.data
|
||||
} catch (e) {
|
||||
ElMessage.error('加载配置失败: ' + e.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
await configFormRef.value.validate()
|
||||
saving.value = true
|
||||
await httpPost('/api/admin/jimeng/config/update', jimengConfig.value)
|
||||
ElMessage.success('配置保存成功!')
|
||||
} catch (e) {
|
||||
if (e.message) {
|
||||
ElMessage.error(e.message)
|
||||
}
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
const resetConfig = () => {
|
||||
jimengConfig.value = {
|
||||
access_key: '',
|
||||
secret_key: '',
|
||||
power: {
|
||||
text_to_image: 10,
|
||||
image_to_image: 15,
|
||||
image_edit: 20,
|
||||
image_effects: 25,
|
||||
text_to_video: 30,
|
||||
image_to_video: 35,
|
||||
},
|
||||
}
|
||||
ElMessage.info('配置已重置')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '../../../assets/css/admin/form.styl'
|
||||
@import '../../../assets/css/main.styl'
|
||||
|
||||
.system-config {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.container {
|
||||
width 100%
|
||||
max-width 800px
|
||||
}
|
||||
|
||||
.label-title {
|
||||
display flex
|
||||
align-items center
|
||||
gap 5px
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user