即梦绘画添加图片特效预览
@@ -333,10 +333,6 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
if job.Status != model.JMTaskStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能删除")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
@@ -345,17 +341,20 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 退回算力
|
||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "jimeng",
|
||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "退回算力失败")
|
||||
tx.Rollback()
|
||||
return
|
||||
// 失败任务删除后退回算力
|
||||
if job.Status != model.JMTaskStatusFailed {
|
||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "jimeng",
|
||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "退回算力失败")
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{})
|
||||
|
||||
@@ -378,7 +378,7 @@ func (s *Service) pollTaskStatus() {
|
||||
|
||||
for _, job := range jobs {
|
||||
// 任务超时处理
|
||||
if job.UpdatedAt.Before(time.Now().Add(-5 * time.Minute)) {
|
||||
if job.UpdatedAt.Before(time.Now().Add(-10 * time.Minute)) {
|
||||
s.handleTaskError(job.Id, "task timeout")
|
||||
continue
|
||||
}
|
||||
@@ -391,7 +391,7 @@ func (s *Service) pollTaskStatus() {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("query jimeng task status failed: %v", err)
|
||||
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -446,9 +446,7 @@ func (s *Service) pollTaskStatus() {
|
||||
s.handleTaskError(job.Id, "task not found")
|
||||
|
||||
case model.JMTaskStatusExpired:
|
||||
// 任务过期
|
||||
s.handleTaskError(job.Id, "task expired")
|
||||
|
||||
continue
|
||||
default:
|
||||
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@better-scroll/pull-up": "^2.5.1",
|
||||
"@better-scroll/scroll-bar": "^2.5.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^0.27.2",
|
||||
"clipboard": "^2.0.11",
|
||||
@@ -41,17 +42,17 @@
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"three": "^0.128.0",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.15",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"vue-waterfall-plugin-next": "^2.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"stylus": "^0.58.1",
|
||||
"stylus-loader": "^7.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
|
||||
4768
web/pnpm-lock.yaml
generated
Normal file
BIN
web/public/images/jimeng/templates/acrylic_ornaments.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
web/public/images/jimeng/templates/angel_figurine.png
Normal file
|
After Width: | Height: | Size: 933 KiB |
BIN
web/public/images/jimeng/templates/felt_3d_polaroid.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
web/public/images/jimeng/templates/felt_keychain.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
web/public/images/jimeng/templates/furry_dream_doll.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
web/public/images/jimeng/templates/glass_ball.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
web/public/images/jimeng/templates/lying_in_fluffy_belly.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 982 KiB |
BIN
web/public/images/jimeng/templates/my_world.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
web/public/images/jimeng/templates/my_world_universal.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
web/public/images/jimeng/templates/plastic_bubble_figure.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -386,7 +386,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
break
|
||||
case 'image_edit':
|
||||
Object.assign(requestData, {
|
||||
image_urls: imageEditParams.image_urls,
|
||||
image_urls: [imageEditParams.image_urls],
|
||||
scale: imageEditParams.scale,
|
||||
seed: imageEditParams.seed,
|
||||
})
|
||||
@@ -397,6 +397,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
template_id: imageEffectsParams.template_id,
|
||||
width: parseInt(imageEffectsParams.size.split('x')[0]),
|
||||
height: parseInt(imageEffectsParams.size.split('x')[1]),
|
||||
prompt: imageEffectsParams.prompt,
|
||||
})
|
||||
break
|
||||
case 'text_to_video':
|
||||
@@ -640,3 +641,68 @@ export const videoAspectRatioOptions = [
|
||||
{ label: '16:9 (横版)', value: '16:9' },
|
||||
{ label: '9:16 (竖版)', value: '9:16' },
|
||||
]
|
||||
|
||||
export const imageEffectsTemplateOptions = [
|
||||
{
|
||||
label: '毛毡3D拍立得风格',
|
||||
value: 'felt_3d_polaroid',
|
||||
preview: '/images/jimeng/templates/felt_3d_polaroid.png',
|
||||
},
|
||||
{ label: '像素世界风', value: 'my_world', preview: '/images/jimeng/templates/my_world.png' },
|
||||
{
|
||||
label: '像素世界-万物通用版',
|
||||
value: 'my_world_universal',
|
||||
preview: '/images/jimeng/templates/my_world_universal.png',
|
||||
},
|
||||
{
|
||||
label: '盲盒玩偶风',
|
||||
value: 'plastic_bubble_figure',
|
||||
preview: '/images/jimeng/templates/plastic_bubble_figure.png',
|
||||
},
|
||||
{
|
||||
label: '塑料泡罩人偶-文字卡头版',
|
||||
value: 'plastic_bubble_figure_cartoon_text',
|
||||
preview: '/images/jimeng/templates/plastic_bubble_figure_cartoon_text.png',
|
||||
},
|
||||
{
|
||||
label: '毛绒玩偶风',
|
||||
value: 'furry_dream_doll',
|
||||
preview: '/images/jimeng/templates/furry_dream_doll.png',
|
||||
},
|
||||
{
|
||||
label: '迷你世界玩偶风',
|
||||
value: 'micro_landscape_mini_world',
|
||||
preview: '/images/jimeng/templates/micro_landscape_mini_world.png',
|
||||
},
|
||||
{
|
||||
label: '微型景观小世界-职业版',
|
||||
value: 'micro_landscape_mini_world_professional',
|
||||
preview: '/images/jimeng/templates/micro_landscape_mini_world_professional.png',
|
||||
},
|
||||
{
|
||||
label: '亚克力挂饰',
|
||||
value: 'acrylic_ornaments',
|
||||
preview: '/images/jimeng/templates/acrylic_ornaments.png',
|
||||
},
|
||||
{
|
||||
label: '毛毡钥匙扣',
|
||||
value: 'felt_keychain',
|
||||
preview: '/images/jimeng/templates/felt_keychain.png',
|
||||
},
|
||||
{
|
||||
label: 'Lofi 像素人物小卡',
|
||||
value: 'lofi_pixel_character_mini_card',
|
||||
preview: '/images/jimeng/templates/lofi_pixel_character_mini_card.png',
|
||||
},
|
||||
{
|
||||
label: '天使形象手办',
|
||||
value: 'angel_figurine',
|
||||
preview: '/images/jimeng/templates/angel_figurine.png',
|
||||
},
|
||||
{
|
||||
label: '躺在毛茸茸肚皮里',
|
||||
value: 'lying_in_fluffy_belly',
|
||||
preview: '/images/jimeng/templates/lying_in_fluffy_belly.png',
|
||||
},
|
||||
{ label: '玻璃球', value: 'glass_ball', preview: '/images/jimeng/templates/glass_ball.png' },
|
||||
]
|
||||
|
||||
@@ -181,10 +181,40 @@
|
||||
<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
|
||||
v-model="store.imageEffectsParams.template_id"
|
||||
placeholder="选择特效模板"
|
||||
popper-class="jimeng-template-select"
|
||||
@change="handleTemplateChange($event)"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="flex items-center py-1">
|
||||
<el-image
|
||||
v-if="templatePreview"
|
||||
:src="templatePreview"
|
||||
class="w-[50px] h-[50px] object-cover rounded-md"
|
||||
:preview-src-list="[templatePreview]"
|
||||
:preview-teleported="true"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="opt in imageEffectsTemplateOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
>
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="template-label">{{ opt.label }}</span>
|
||||
<img
|
||||
v-if="opt.preview"
|
||||
:src="opt.preview"
|
||||
:alt="opt.label"
|
||||
class="w-[50px] h-[50px] object-cover rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -444,17 +474,17 @@
|
||||
></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">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<i
|
||||
class="iconfont icon-remove cursor-pointer text-red-500"
|
||||
@click="store.removeJob(item)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="ml-1" v-if="item.video_url || item.img_url">
|
||||
<el-tooltip content="下载" placement="top">
|
||||
<i
|
||||
@@ -518,8 +548,14 @@
|
||||
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 {
|
||||
imageEffectsTemplateOptions,
|
||||
imageSizeOptions,
|
||||
useJimengStore,
|
||||
videoAspectRatioOptions,
|
||||
} from '@/store/jimeng'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import { Switch } from '@element-plus/icons-vue'
|
||||
@@ -546,6 +582,8 @@ const store = useJimengStore()
|
||||
|
||||
// 新增:瀑布流渲染完成状态
|
||||
const waterfallRendered = ref(false)
|
||||
// 新增:模板预览图
|
||||
const templatePreview = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
@@ -574,6 +612,13 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
function handleTemplateChange(value) {
|
||||
templatePreview.value = imageEffectsTemplateOptions.find((opt) => opt.value === value)?.preview
|
||||
store.imageEffectsParams.prompt = imageEffectsTemplateOptions.find(
|
||||
(opt) => opt.value === value
|
||||
)?.label
|
||||
}
|
||||
|
||||
function onWaterfallAfterRender() {
|
||||
waterfallRendered.value = true
|
||||
if (!store.loading && !store.isOver) {
|
||||
@@ -604,7 +649,7 @@ function copyErrorMsg(msg) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.task-list {
|
||||
.task-grid {
|
||||
display: grid;
|
||||
@@ -614,8 +659,9 @@ function copyErrorMsg(msg) {
|
||||
}
|
||||
// 新增:增强任务项悬停动画
|
||||
.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);
|
||||
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;
|
||||
@@ -623,7 +669,7 @@ function copyErrorMsg(msg) {
|
||||
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);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 1.5px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: #a259ff;
|
||||
transform: scale(1.025) translateY(-2px);
|
||||
z-index: 10;
|
||||
@@ -640,49 +686,68 @@ function copyErrorMsg(msg) {
|
||||
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
|
||||
.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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.jimeng-template-select {
|
||||
.el-select-dropdown__item {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||