即梦绘画添加图片特效预览

This commit is contained in:
GeekMaster
2025-07-29 18:34:29 +08:00
parent ff96fada02
commit 9fba68fb14
20 changed files with 4982 additions and 85 deletions

View File

@@ -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{})

View File

@@ -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)
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -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' },
]

View File

@@ -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>