完成新瀑布流组件整合

This commit is contained in:
RockYang 2025-04-03 16:48:56 +08:00
parent 0746cd49f4
commit c797b35f5a
10 changed files with 1350 additions and 535 deletions

View File

@ -5,8 +5,8 @@
- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。 - 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。
- 功能优化:支持原生的 DeepSeek 推理模型 API聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions - 功能优化:支持原生的 DeepSeek 推理模型 API聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions
- 功能优化:支持 GPT-4o 图片编辑功能。 - 功能优化:支持 GPT-4o 图片编辑功能。
- 功能新增对话页面支持AI输出语音播报TTS - 功能新增:对话页面支持 AI 输出语音播报TTS
- 功能新增:支持 Goole 账号登录 - 功能优化:替换瀑布流组件,优化用户体验
## v4.2.1 ## v4.2.1

View File

@ -112,19 +112,23 @@ func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
method := c.Request.Method method := c.Request.Method
origin := c.Request.Header.Get("Origin") origin := c.Request.Header.Get("Origin")
// 设置允许的请求源
if origin != "" { if origin != "" {
// 设置允许的请求源
c.Header("Access-Control-Allow-Origin", origin) c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") } else {
//允许跨域设置可以返回其他子段,可以自定义字段 c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
} }
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
if method == http.MethodOptions { if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!") c.JSON(http.StatusOK, "ok!")

View File

@ -32,14 +32,13 @@
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.1", "qs": "^6.11.1",
"@better-scroll/core": "^2.5.1", "@better-scroll/core": "^2.5.1",
"@better-scroll/mouse-wheel": "^2.5.1", "@better-scroll/mouse-wheel": "^2.5.1",
"@better-scroll/observe-dom": "^2.5.1", "@better-scroll/observe-dom": "^2.5.1",
"@better-scroll/pull-up": "^2.5.1", "@better-scroll/pull-up": "^2.5.1",
"@better-scroll/scroll-bar": "^2.5.1", "@better-scroll/scroll-bar": "^2.5.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"three": "^0.128.0", "three": "^0.128.0",
"v3-waterfall": "^1.3.3",
"vant": "^4.5.0", "vant": "^4.5.0",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.15", "vue-router": "^4.0.15",

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -1,5 +1,82 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import Storage from "good-storage"; import Storage from "good-storage";
import errorIcon from "@/assets/img/failed.png";
import loadingIcon from "@/assets/img/loading.gif";
let waterfallOptions = {
// 唯一key值
rowKey: "id",
// 卡片之间的间隙
gutter: 10,
// 是否有周围的gutter
hasAroundGutter: true,
// 卡片在PC上的宽度
width: 200,
// 自定义行显示个数,主要用于对移动端的适配
breakpoints: {
3840: {
// 4K下
rowPerView: 8,
},
2560: {
// 2K下
rowPerView: 7,
},
1920: {
// 2K下
rowPerView: 6,
},
1600: {
// 2K下
rowPerView: 5,
},
1366: {
// 2K下
rowPerView: 4,
},
800: {
// 当屏幕宽度小于等于800
rowPerView: 3,
},
500: {
// 当屏幕宽度小于等于500
rowPerView: 2,
},
},
// 动画效果
animationEffect: "animate__fadeInUp",
// 动画时间
animationDuration: 1000,
// 动画延迟
animationDelay: 300,
animationCancel: false,
// 背景色
backgroundColor: "",
// imgSelector
imgSelector: "img_thumb",
// 是否跨域
crossOrigin: true,
// 加载配置
loadProps: {
loading: loadingIcon,
error: errorIcon,
ratioCalculator: (width, height) => {
const minRatio = 3 / 4;
const maxRatio = 4 / 3;
const curRatio = height / width;
if (curRatio < minRatio) {
return minRatio;
} else if (curRatio > maxRatio) {
return maxRatio;
} else {
return curRatio;
}
},
},
// 是否懒加载
lazyload: true,
align: "center",
}
export const useSharedStore = defineStore("shared", { export const useSharedStore = defineStore("shared", {
state: () => ({ state: () => ({
@ -11,6 +88,7 @@ export const useSharedStore = defineStore("shared", {
isLogin: false, isLogin: false,
chatListExtend: Storage.get("chat_list_extend", true), chatListExtend: Storage.get("chat_list_extend", true),
ttsModel: Storage.get("tts_model", ""), ttsModel: Storage.get("tts_model", ""),
waterfallOptions,
}), }),
getters: {}, getters: {},
actions: { actions: {

View File

@ -11,8 +11,18 @@
<el-form-item label="生图模型"> <el-form-item label="生图模型">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="selectedModel" style="width: 150px" placeholder="请选择模型" @change="changeModel"> <el-select
<el-option v-for="v in models" :label="v.name" :value="v" :key="v.value" /> v-model="selectedModel"
style="width: 150px"
placeholder="请选择模型"
@change="changeModel"
>
<el-option
v-for="v in models"
:label="v.name"
:value="v"
:key="v.value"
/>
</el-select> </el-select>
</div> </div>
</template> </template>
@ -24,7 +34,12 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.quality" style="width: 150px"> <el-select v-model="params.quality" style="width: 150px">
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value" /> <el-option
v-for="v in qualities"
:label="v.name"
:value="v.value"
:key="v.value"
/>
</el-select> </el-select>
</div> </div>
</template> </template>
@ -36,7 +51,12 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.size" style="width: 150px"> <el-select v-model="params.size" style="width: 150px">
<el-option v-for="v in sizes" :label="v" :value="v" :key="v" /> <el-option
v-for="v in sizes"
:label="v"
:value="v"
:key="v"
/>
</el-select> </el-select>
</div> </div>
</template> </template>
@ -48,9 +68,18 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.style" style="width: 150px"> <el-select v-model="params.style" style="width: 150px">
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value" /> <el-option
v-for="v in styles"
:label="v.name"
:value="v.value"
:key="v.value"
/>
</el-select> </el-select>
<el-tooltip content="生动使模型倾向于生成超真实和戏剧性的图像" raw-content placement="right"> <el-tooltip
content="生动使模型倾向于生成超真实和戏剧性的图像"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -73,8 +102,17 @@
</div> </div>
<el-row class="text-info"> <el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating"> <el-button
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i> class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i
class="iconfont icon-chuangzuo"
style="margin-right: 5px"
></i>
<span>生成专业绘画指令</span> <span>生成专业绘画指令</span>
</el-button> </el-button>
</el-row> </el-row>
@ -82,7 +120,10 @@
<div class="text-info"> <div class="text-info">
<el-row :gutter="10"> <el-row :gutter="10">
<el-text type="primary" <el-text type="primary"
>每次绘图消耗 <el-text type="warning">{{ dallPower }}算力</el-text></el-text >每次绘图消耗
<el-text type="warning"
>{{ dallPower }}算力</el-text
></el-text
> >
<el-text type="primary" <el-text type="primary"
>当前可用 >当前可用
@ -93,7 +134,9 @@
</el-form> </el-form>
</div> </div>
<div class="submit-btn"> <div class="submit-btn">
<el-button type="primary" :dark="false" round @click="generate"> 立即生成 </el-button> <el-button type="primary" :dark="false" round @click="generate">
立即生成
</el-button>
</div> </div>
</div> </div>
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh"> <div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
@ -103,154 +146,162 @@
<task-list :list="runningJobs" /> <task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0"> <template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2> <h2 class="text-xl">创作记录</h2>
<div class="finish-job-list"> <div class="finish-job-list mt-3">
<div v-if="finishedJobs.length > 0"> <div v-if="finishedJobs.length > 0">
<!-- <v3-waterfall <Waterfall
id="waterfall"
:list="finishedJobs" :list="finishedJobs"
srcKey="img_thumb" :row-key="waterfallOptions.rowKey"
:gap="20" :gutter="waterfallOptions.gutter"
:bottomGap="-10" :has-around-gutter="waterfallOptions.hasAroundGutter"
:colWidth="colWidth" :width="waterfallOptions.width"
:distanceToScroll="100" :breakpoints="waterfallOptions.breakpoints"
:isLoading="loading" :img-selector="waterfallOptions.imgSelector"
:isOver="isOver" :background-color="waterfallOptions.backgroundColor"
@scrollReachBottom="fetchFinishJobs()" :animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
> >
<template #default="slotProp"> <template #default="{ item, url }">
<div class="job-item"> <div
<el-image class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
v-if="slotProp.item.img_url !== ''" >
@click="previewImg(slotProp.item)" <div class="overflow-hidden rounded-lg">
:src="slotProp.item['img_thumb']" <LazyImg
fit="cover" :url="url"
loading="lazy" v-if="item.progress === 100"
> class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
<template #placeholder> @click="previewImg(item)"
<div class="image-slot">正在加载图片</div> />
</template> <el-image v-else-if="item.progress === 101">
<template #error>
<template #error> <div class="image-slot">
<div class="image-slot"> <div class="err-msg-container">
<el-icon> <div class="title">任务失败</div>
<Picture /> <div class="opt">
</el-icon> <el-popover
</div> title="错误详情"
</template> trigger="click"
</el-image> :width="250"
:content="item['err_msg']"
<el-image v-else-if="slotProp.item.progress === 101"> placement="top"
<template #error> >
<div class="image-slot"> <template #reference>
<div class="err-msg-container"> <el-button type="info"
<div class="title">任务失败</div> >详情</el-button
<div class="opt"> >
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top"> </template>
<template #reference> </el-popover>
<el-button type="info">详情</el-button> <el-button
</template> type="danger"
</el-popover> @click="removeImage(item)"
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button> >删除</el-button
>
</div>
</div> </div>
</div> </div>
</template>
</el-image>
</div>
<div
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
v-if="item.progress === 100"
>
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="flex">
<el-tooltip
content="取消分享"
placement="top"
v-if="item.publish"
>
<el-button
type="warning"
@click="publishImage(item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip
content="分享"
placement="top"
v-else
>
<el-button
type="success"
@click="publishImage(item, true)"
circle
>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(item)"
circle
/>
</el-tooltip>
</div> </div>
</template> </div>
</el-image>
<el-image v-else>
<template #error>
<div class="image-slot">
<i class="iconfont icon-loading"></i>
<span>正在下载图片</span>
</div>
</template>
</el-image>
<div class="remove">
<el-tooltip content="删除" placement="top">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
</el-tooltip>
<el-tooltip content="取消分享" placement="top" v-if="slotProp.item.publish">
<el-button type="warning" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="分享" placement="top" v-else>
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button type="info" circle class="copy-prompt" :data-clipboard-text="slotProp.item.prompt">
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
</div> </div>
</div> </div>
</template> </template>
</Waterfall>
<template #footer> <div class="flex justify-center py-10">
<div class="no-more-data"> <img
<span>没有更多数据了</span> :src="waterfallOptions.loadProps.loading"
class="max-w-[50px] max-h-[50px]"
v-if="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="fetchFinishJobs"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i> <i class="iconfont icon-face"></i>
</div> </div>
</template> </div>
</v3-waterfall> --> </div>
<Waterfall
ref="waterfall"
:list="finishedJobs"
:row-key="options.rowKey"
:gutter="options.gutter"
:has-around-gutter="options.hasAroundGutter"
:width="options.width"
:breakpoints="options.breakpoints"
:img-selector="options.imgSelector"
:background-color="options.backgroundColor"
:animation-effect="options.animationEffect"
:animation-duration="options.animationDuration"
:animation-delay="options.animationDelay"
:animation-cancel="options.animationCancel"
:lazyload="options.lazyload"
:load-props="options.loadProps"
:cross-origin="options.crossOrigin"
:align="options.align"
@afterRender="afterRender"
>
<template #default="{ item, url, index }">
<div class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-lg hover:shadow-gray-600 group" @click="handleClick(item)">
<div class="overflow-hidden">
<LazyImg :url="url" title="title" :alt="item.name" class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105" @load="imageLoad" @error="imageError" @success="imageSuccess" />
</div>
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<h2 class="pb-4 text-gray-50 group-hover:text-yellow-300">
{{ item.name }}
</h2>
<div class="pt-3 flex justify-between items-center border-t border-t-gray-600 border-opacity-50">
<div class="text-gray-50">
$ {{ item.price }}
</div>
<div>
<button class="px-3 h-7 rounded-full bg-red-500 text-sm text-white shadow-lg transition-all duration-300 hover:bg-red-600" @click.stop="handleDelete(item, index)">
删除
</button>
</div>
</div>
</div>
</div>
</template>
</Waterfall>
</div> </div>
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else /> <el-empty
:image-size="100"
<div v-show="!loading" class="flex justify-center py-10 bg-gray-900"> :image="nodata"
<button class="px-5 py-2 rounded-full bg-gray-700 text-md text-white cursor-pointer hover:bg-gray-800 transition-all duration-300" @click="fetchFinishJobs"> description="暂无记录"
加载更多 v-else
</button> />
</div>
</div> </div>
</template> </template>
<!-- end finish job list--> <!-- end finish job list-->
</div> </div>
</div> </div>
@ -285,109 +336,22 @@ import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import { showMessageError, showMessageOK } from "@/utils/dialog"; import { showMessageError, showMessageOK } from "@/utils/dialog";
import BScrollBox from "@/components/ui/BScrollBox.vue"; import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next' import "vue-waterfall-plugin-next/dist/style.css";
import 'vue-waterfall-plugin-next/dist/style.css'
import error from '@/assets/img/failed.png'
const listBoxHeight = ref(0); const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0) // const paramBoxHeight = ref(0)
const isLogin = ref(false); const isLogin = ref(false);
const loading = ref(true); const loading = ref(true);
const colWidth = ref(220);
const isOver = ref(false); const isOver = ref(false);
const previewURL = ref(""); const previewURL = ref("");
const store = useSharedStore(); const store = useSharedStore();
const models = ref([]); const models = ref([]);
const waterfallOptions = store.waterfallOptions;
const resizeElement = function () { const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 58; listBoxHeight.value = window.innerHeight - 58;
// paramBoxHeight.value = window.innerHeight - 110
}; };
const options = ref({
// key
rowKey: 'id',
//
gutter: 10,
// gutter
hasAroundGutter: true,
// PC
width: 200,
//
breakpoints: {
3840: {
// 4K
rowPerView: 8,
},
2560: {
// 2K
rowPerView: 7,
},
1920: {
// 2K
rowPerView: 6,
},
1600: {
// 2K
rowPerView: 5,
},
1366: {
// 2K
rowPerView: 4,
},
800: {
// 800
rowPerView: 3,
},
500: {
// 500
rowPerView: 2,
},
},
//
animationEffect: 'animate__fadeInUp',
//
animationDuration: 1000,
//
animationDelay: 300,
animationCancel: false,
//
backgroundColor: '#2C2E3A',
// imgSelector
imgSelector: 'img_thumb',
//
loadProps: {
loading,
error,
ratioCalculator: (width, height) => {
console.log("width, height", width, height)
return height / width
},
},
//
lazyload: true,
align: 'center',
})
function imageLoad(url) {
console.log(`${url}: 加载完成`)
}
function imageError(url) {
console.error(`${url}: 加载失败`)
}
function imageSuccess(url) {
console.log(`${url}: 加载成功`)
}
function afterRender() {
loading.value = false
console.log('计算完成')
}
resizeElement(); resizeElement();
window.onresize = () => { window.onresize = () => {
resizeElement(); resizeElement();
@ -397,7 +361,15 @@ const qualities = [
{ name: "高清", value: "hd" }, { name: "高清", value: "hd" },
]; ];
const dalleSizes = ["1024x1024", "1792x1024", "1024x1792"]; const dalleSizes = ["1024x1024", "1792x1024", "1024x1792"];
const fluxSizes = ["1024x1024", "1024x768", "768x1024", "1280x960", "960x1280", "1366x768", "768x1366"]; const fluxSizes = [
"1024x1024",
"1024x768",
"768x1024",
"1280x960",
"960x1280",
"1366x768",
"768x1366",
];
const sizes = ref(dalleSizes); const sizes = ref(dalleSizes);
const styles = [ const styles = [
{ name: "生动", value: "vivid" }, { name: "生动", value: "vivid" },
@ -487,7 +459,10 @@ const fetchRunningJobs = () => {
httpGet(`/api/dall/jobs?finish=false`) httpGet(`/api/dall/jobs?finish=false`)
.then((res) => { .then((res) => {
// //
if (res.data.items && res.data.items.length !== runningJobs.value.length) { if (
res.data.items &&
res.data.items.length !== runningJobs.value.length
) {
page.value = 0; page.value = 0;
fetchFinishJobs(); fetchFinishJobs();
} }
@ -514,25 +489,28 @@ const fetchFinishJobs = () => {
loading.value = true; loading.value = true;
page.value = page.value + 1; page.value = page.value + 1;
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`) httpGet(
`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`
)
.then((res) => { .then((res) => {
if (res.data.items.length < pageSize.value) { if (res.data.items.length < pageSize.value) {
isOver.value = true; isOver.value = true;
loading.value = false;
} }
const imageList = res.data.items; const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) { for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"; imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
} }
if (page.value === 1) { if (page.value === 1) {
finishedJobs.value = imageList; finishedJobs.value = imageList;
} else { } else {
finishedJobs.value = finishedJobs.value.concat(imageList); finishedJobs.value = finishedJobs.value.concat(imageList);
} }
loading.value = false;
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取任务失败:" + e.message); ElMessage.error("获取任务失败:" + e.message);
loading.value = false;
}); });
}; };
@ -635,6 +613,6 @@ const changeModel = (model) => {
</script> </script>
<style lang="stylus"> <style lang="stylus">
@import "@/assets/css/image-dall.styl" @import '@/assets/css/image-dall.styl';
@import "@/assets/css/custom-scroll.styl" @import '@/assets/css/custom-scroll.styl';
</style> </style>

View File

@ -18,8 +18,20 @@
<div class="param-line pt"> <div class="param-line pt">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="8" v-for="item in rates" :key="item.value"> <el-col :span="8" v-for="item in rates" :key="item.value">
<div class="flex-col items-center" :class="item.value === params.rate ? 'grid-content active' : 'grid-content'" @click="changeRate(item)"> <div
<el-image class="icon" :src="item.img" fit="cover"></el-image> class="flex-col items-center"
:class="
item.value === params.rate
? 'grid-content active'
: 'grid-content'
"
@click="changeRate(item)"
>
<el-image
class="icon"
:src="item.img"
fit="cover"
></el-image>
<div class="text">{{ item.text }}</div> <div class="text">{{ item.text }}</div>
</div> </div>
</el-col> </el-col>
@ -30,10 +42,23 @@
<el-form-item label="图片画质"> <el-form-item label="图片画质">
<template #default> <template #default>
<div class="form-item-inner flex-row items-center"> <div class="form-item-inner flex-row items-center">
<el-select v-model="params.quality" placeholder="请选择" style="width: 150px"> <el-select
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> v-model="params.quality"
placeholder="请选择"
style="width: 150px"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select> </el-select>
<el-tooltip content="生成的图片质量,质量越好出图越慢" placement="right"> <el-tooltip
content="生成的图片质量,质量越好出图越慢"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -45,7 +70,11 @@
<div class="param-line pt"> <div class="param-line pt">
<span>模型选择</span> <span>模型选择</span>
<el-tooltip content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型" raw-content placement="right"> <el-tooltip
content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型"
raw-content
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -54,7 +83,12 @@
<div class="param-line pt"> <div class="param-line pt">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12" v-for="item in models" :key="item.value"> <el-col :span="12" v-for="item in models" :key="item.value">
<div :class="item.value === params.model ? 'model active' : 'model'" @click="changeModel(item)"> <div
:class="
item.value === params.model ? 'model active' : 'model'
"
@click="changeModel(item)"
>
<el-image :src="item.img" fit="cover"></el-image> <el-image :src="item.img" fit="cover"></el-image>
<div class="text">{{ item.text }}</div> <div class="text">{{ item.text }}</div>
</div> </div>
@ -67,7 +101,11 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-switch v-model="params.tile" inactive-color="#464649" /> <el-switch v-model="params.tile" inactive-color="#464649" />
<el-tooltip content="重复:--tile参数释义生成可用作重复平铺的图像以创建无缝图案。" raw-content placement="right"> <el-tooltip
content="重复:--tile参数释义生成可用作重复平铺的图像以创建无缝图案。"
raw-content
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -100,7 +138,12 @@
<el-form-item label="创意度"> <el-form-item label="创意度">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-slider v-model.number="params.chaos" :max="100" :step="1" style="width: 180px" /> <el-slider
v-model.number="params.chaos"
:max="100"
:step="1"
style="width: 180px"
/>
<el-tooltip <el-tooltip
content="参数用法:--chaos 或--c取值范围: 0-100 <br/> 取值越高结果越发散,反之则稳定收敛<br /> 默认值0最为精准稳定" content="参数用法:--chaos 或--c取值范围: 0-100 <br/> 取值越高结果越发散,反之则稳定收敛<br /> 默认值0最为精准稳定"
raw-content raw-content
@ -119,7 +162,13 @@
<el-form-item label="风格化"> <el-form-item label="风格化">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-slider v-model.number="params.stylize" :min="0" :max="1000" :step="1" style="width: 180px" /> <el-slider
v-model.number="params.stylize"
:min="0"
:max="1000"
:step="1"
style="width: 180px"
/>
<el-tooltip <el-tooltip
content="风格化:--stylize 或 --s范围 1-1000默认值100 <br/>高取值会产生非常艺术化但与提示关联性较低的图像" content="风格化:--stylize 或 --s范围 1-1000默认值100 <br/>高取值会产生非常艺术化但与提示关联性较低的图像"
raw-content raw-content
@ -139,7 +188,11 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-input v-model.number="params.seed" /> <el-input v-model.number="params.seed" />
<el-tooltip content="随机种子:--seed默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像" raw-content placement="right"> <el-tooltip
content="随机种子:--seed默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像"
raw-content
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -155,14 +208,21 @@
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }"> <div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<div class="extra-params"> <div class="extra-params">
<el-form> <el-form>
<el-tabs v-model="activeName" class="title-tabs" @tabChange="tabChange"> <el-tabs
v-model="activeName"
class="title-tabs"
@tabChange="tabChange"
>
<el-tab-pane label="文生图" name="txt2img"> <el-tab-pane label="文生图" name="txt2img">
<div class="prompt-box"> <div class="prompt-box">
<div class="param-line pt"> <div class="param-line pt">
<div class="flex-row justify-between items-center"> <div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center"> <div class="flex-row justify-start items-center">
<span>提示词</span> <span>提示词</span>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right"> <el-tooltip
content="输入你想要的内容,用逗号分割"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -184,7 +244,13 @@
</div> </div>
<el-row class="text-info"> <el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating"> <el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i class="iconfont icon-chuangzuo"></i> <i class="iconfont icon-chuangzuo"></i>
<span>生成专业绘画指令</span> <span>生成专业绘画指令</span>
</el-button> </el-button>
@ -194,7 +260,10 @@
<div class="flex-row justify-between items-center"> <div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center"> <div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span> <span>不希望出现的内容可选</span>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right"> <el-tooltip
content="不想出现在图片上的元素(例如:树,建筑)"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -216,16 +285,33 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="图生图" name="img2img"> <el-tab-pane label="图生图" name="img2img">
<div class="text">图生图以某张图片为底稿参考来创作绘画生成类似风格或类型图像支持 PNG JPG 格式图片</div> <div class="text">
图生图以某张图片为底稿参考来创作绘画生成类似风格或类型图像支持
PNG JPG 格式图片
</div>
<div class="param-line"> <div class="param-line">
<div class="img-inline"> <div class="img-inline">
<div class="img-list-box"> <div class="img-list-box">
<div class="img-item" v-for="imgURL in imgList" :key="imgURL"> <div
class="img-item"
v-for="imgURL in imgList"
:key="imgURL"
>
<el-image :src="imgURL" fit="cover" /> <el-image :src="imgURL" fit="cover" />
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle /> <el-button
type="danger"
:icon="Delete"
@click="removeUploadImage(imgURL)"
circle
/>
</div> </div>
</div> </div>
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadImg"> <el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-icon class="uploader-icon"> <el-icon class="uploader-icon">
<Plus /> <Plus />
</el-icon> </el-icon>
@ -237,7 +323,12 @@
<el-form-item label="参考权重:"> <el-form-item label="参考权重:">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-slider v-model.number="params.iw" :max="1" :step="0.01" style="width: 180px" /> <el-slider
v-model.number="params.iw"
:max="1"
:step="0.01"
style="width: 180px"
/>
<el-tooltip <el-tooltip
content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响" content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响"
raw-content raw-content
@ -257,7 +348,10 @@
<div class="flex-row justify-between items-center"> <div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center"> <div class="flex-row justify-start items-center">
<span>提示词</span> <span>提示词</span>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right"> <el-tooltip
content="输入你想要的内容,用逗号分割"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -278,7 +372,13 @@
</div> </div>
<el-row class="text-info"> <el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating"> <el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i class="iconfont icon-chuangzuo"></i> <i class="iconfont icon-chuangzuo"></i>
<span>生成专业绘画指令</span> <span>生成专业绘画指令</span>
</el-button> </el-button>
@ -288,7 +388,10 @@
<div class="flex-row justify-between items-center"> <div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center"> <div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span> <span>不希望出现的内容可选</span>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right"> <el-tooltip
content="不想出现在图片上的元素(例如:树,建筑)"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -310,15 +413,31 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="融图" name="blend"> <el-tab-pane label="融图" name="blend">
<div class="text">请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能</div> <div class="text">
请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能
</div>
<div class="img-inline"> <div class="img-inline">
<div class="img-list-box"> <div class="img-list-box">
<div class="img-item" v-for="imgURL in imgList" :key="imgURL"> <div
class="img-item"
v-for="imgURL in imgList"
:key="imgURL"
>
<el-image :src="imgURL" fit="cover" /> <el-image :src="imgURL" fit="cover" />
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle /> <el-button
type="danger"
:icon="Delete"
@click="removeUploadImage(imgURL)"
circle
/>
</div> </div>
</div> </div>
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadImg"> <el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-icon class="uploader-icon"> <el-icon class="uploader-icon">
<Plus /> <Plus />
</el-icon> </el-icon>
@ -327,15 +446,31 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="换脸" name="swapFace"> <el-tab-pane label="换脸" name="swapFace">
<div class="text">请上传两张有脸部的图片用左边图片的脸替换右边图片的脸</div> <div class="text">
请上传两张有脸部的图片用左边图片的脸替换右边图片的脸
</div>
<div class="img-inline"> <div class="img-inline">
<div class="img-list-box"> <div class="img-list-box">
<div class="img-item" v-for="imgURL in imgList" :key="imgURL"> <div
class="img-item"
v-for="imgURL in imgList"
:key="imgURL"
>
<el-image :src="imgURL" fit="cover" /> <el-image :src="imgURL" fit="cover" />
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle /> <el-button
type="danger"
:icon="Delete"
@click="removeUploadImage(imgURL)"
circle
/>
</div> </div>
</div> </div>
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadImg"> <el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-icon class="uploader-icon"> <el-icon class="uploader-icon">
<Plus /> <Plus />
</el-icon> </el-icon>
@ -350,17 +485,29 @@
</el-badge> </el-badge>
</template> </template>
<div class="text">注意只有于 niji6 v6 模型支持一致性功能如果选择其他模型此功能将会生成失败</div> <div class="text">
注意只有于 niji6 v6
模型支持一致性功能如果选择其他模型此功能将会生成失败
</div>
<div class="param-line"> <div class="param-line">
<el-form-item label="角色一致性:" prop="cref"> <el-form-item label="角色一致性:" prop="cref">
<el-input <el-input
v-model="params.cref" v-model="params.cref"
placeholder="请输入图片URL或者上传图片" placeholder="请输入图片URL或者上传图片"
style="--el-input-focus-border-color: #b0a0f8; max-width: 500px; width: 100%" style="
--el-input-focus-border-color: #b0a0f8;
max-width: 500px;
width: 100%;
"
size="small" size="small"
> >
<template #append> <template #append>
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('cref')" :http-request="uploadImg"> <el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('cref')"
:http-request="uploadImg"
>
<el-icon class="uploader-icon"> <el-icon class="uploader-icon">
<UploadFilled /> <UploadFilled />
</el-icon> </el-icon>
@ -375,11 +522,20 @@
<el-input <el-input
v-model="params.sref" v-model="params.sref"
placeholder="请输入图片URL或者上传图片" placeholder="请输入图片URL或者上传图片"
style="--el-input-focus-border-color: #b0a0f8; max-width: 500px; width: 100%" style="
--el-input-focus-border-color: #b0a0f8;
max-width: 500px;
width: 100%;
"
size="small" size="small"
> >
<template #append> <template #append>
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('sref')" :http-request="uploadImg"> <el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('sref')"
:http-request="uploadImg"
>
<el-icon class="uploader-icon"> <el-icon class="uploader-icon">
<UploadFilled /> <UploadFilled />
</el-icon> </el-icon>
@ -393,8 +549,17 @@
<el-form-item label="参考权重:"> <el-form-item label="参考权重:">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-slider v-model.number="params.cw" :max="100" :step="1" style="width: 180px" /> <el-slider
<el-tooltip content="取值范围 0-100 <br/>默认值100参考原图的脸部、头发和衣服<br/>0则表示只换脸" raw-content placement="right"> v-model.number="params.cw"
:max="100"
:step="1"
style="width: 180px"
/>
<el-tooltip
content="取值范围 0-100 <br/>默认值100参考原图的脸部、头发和衣服<br/>0则表示只换脸"
raw-content
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -409,7 +574,10 @@
<div class="flex-row justify-between items-center"> <div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center"> <div class="flex-row justify-start items-center">
<span>提示词</span> <span>提示词</span>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right"> <el-tooltip
content="输入你想要的内容,用逗号分割"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -432,7 +600,10 @@
<div class="flex-row justify-between items-center"> <div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center"> <div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span> <span>不希望出现的内容可选</span>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right"> <el-tooltip
content="不想出现在图片上的元素(例如:树,建筑)"
placement="right"
>
<el-icon> <el-icon>
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -458,15 +629,21 @@
<el-text type="primary" <el-text type="primary"
>每次绘图消耗 >每次绘图消耗
<el-text type="warning">{{ mjPower }}算力;</el-text> <el-text type="warning">{{ mjPower }}算力;</el-text>
&nbsp;&nbsp; U/V 操作消耗<el-text type="warning">{{ mjActionPower }}算力;</el-text> </el-text &nbsp;&nbsp; U/V 操作消耗<el-text type="warning"
>{{ mjActionPower }}算力;</el-text
> </el-text
>&nbsp;&nbsp; >&nbsp;&nbsp;
<el-text type="primary" <el-text type="primary"
>当前可用算力<el-text type="warning">{{ power }}</el-text></el-text >当前可用算力<el-text type="warning">{{
power
}}</el-text></el-text
> >
</el-row> </el-row>
<div class="submit-btn"> <div class="submit-btn">
<el-button type="primary" :dark="false" @click="generate" round>立即生成</el-button> <el-button type="primary" :dark="false" @click="generate" round
>立即生成</el-button
>
</div> </div>
</el-form> </el-form>
</div> </div>
@ -476,9 +653,235 @@
<task-list :list="runningJobs" /> <task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0"> <template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2> <h2 class="text-xl">创作记录</h2>
<div class="finish-job-list"> <div class="finish-job-list mt-3">
<div v-if="finishedJobs.length > 0"> <div v-if="finishedJobs.length > 0">
<v3-waterfall <Waterfall
:list="finishedJobs"
:row-key="waterfallOptions.rowKey"
:gutter="waterfallOptions.gutter"
:has-around-gutter="waterfallOptions.hasAroundGutter"
:width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
v-if="item.progress === 100"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="previewImg(item)"
/>
<el-image v-else-if="item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="item['err_msg']"
placement="top"
>
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button
type="danger"
@click="removeImage(item)"
>删除</el-button
>
</div>
</div>
</div>
</template>
</el-image>
</div>
<div
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
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"
>
<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)"
>
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="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>
</div>
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="flex">
<el-tooltip
content="取消分享"
placement="top"
v-if="item.publish"
>
<el-button
type="warning"
@click="publishImage(item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="分享" placement="top" v-else>
<el-button
type="success"
@click="publishImage(item, true)"
circle
>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button
type="info"
circle
class="copy-prompt"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(item)"
circle
/>
</el-tooltip>
</div>
</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="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="fetchFinishJobs"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</div>
</div>
<!-- <v3-waterfall
id="waterfall" id="waterfall"
:list="finishedJobs" :list="finishedJobs"
srcKey="thumb_url" srcKey="thumb_url"
@ -622,9 +1025,14 @@
<i class="iconfont icon-face"></i> <i class="iconfont icon-face"></i>
</div> </div>
</template> </template>
</v3-waterfall> </v3-waterfall> -->
</div> </div>
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else /> <el-empty
:image-size="100"
:image="nodata"
description="暂无记录"
v-else
/>
</div> </div>
</template> </template>
@ -650,7 +1058,15 @@
<script setup> <script setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue"; import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { ChromeFilled, Delete, DocumentCopy, InfoFilled, Picture, Plus, UploadFilled } from "@element-plus/icons-vue"; import {
ChromeFilled,
Delete,
DocumentCopy,
InfoFilled,
Picture,
Plus,
UploadFilled,
} from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png"; import nodata from "@/assets/img/no-data.png";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import { httpGet, httpPost } from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
@ -664,13 +1080,15 @@ import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import { closeLoading, showLoading, showMessageError } from "@/utils/dialog"; import { closeLoading, showLoading, showMessageError } from "@/utils/dialog";
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import "vue-waterfall-plugin-next/dist/style.css";
const listBoxHeight = ref(0); const listBoxHeight = ref(0);
const paramBoxHeight = ref(0); const paramBoxHeight = ref(0);
const loading = ref(true); const loading = ref(true);
const colWidth = ref(220);
const previewURL = ref(""); const previewURL = ref("");
const store = useSharedStore(); const store = useSharedStore();
const waterfallOptions = store.waterfallOptions;
const resizeElement = function () { const resizeElement = function () {
// listBoxHeight.value = window.innerHeight - 80; // listBoxHeight.value = window.innerHeight - 80;
@ -893,22 +1311,26 @@ const fetchFinishJobs = () => {
loading.value = true; loading.value = true;
page.value = page.value + 1; page.value = page.value + 1;
// //
httpGet(`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`) httpGet(
`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`
)
.then((res) => { .then((res) => {
const jobs = res.data.items; const jobs = res.data.items;
let hasDownload = false; let hasDownload = false;
for (let i = 0; i < jobs.length; i++) { for (let i = 0; i < jobs.length; i++) {
if (jobs[i]["img_url"] !== "") { if (jobs[i]["img_url"] !== "") {
if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") { if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75"; jobs[i]["img_thumb"] =
jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
} else { } else {
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75"; jobs[i]["img_thumb"] =
jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
} }
} else { } else {
if (jobs[i].progress === 100) { if (jobs[i].progress === 100) {
hasDownload = true; hasDownload = true;
} }
jobs[i]["thumb_url"] = "/images/img-placeholder.jpg"; jobs[i]["img_thumb"] = "/images/img-placeholder.jpg";
} }
// //
if (page.value === 1) { if (page.value === 1) {
@ -933,10 +1355,10 @@ const fetchFinishJobs = () => {
} else { } else {
finishedJobs.value = finishedJobs.value.concat(jobs); finishedJobs.value = finishedJobs.value.concat(jobs);
} }
loading.value = false;
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取任务失败:" + e.message); ElMessage.error("获取任务失败:" + e.message);
loading.value = false;
}); });
}; };
@ -1139,6 +1561,6 @@ const generatePrompt = () => {
</script> </script>
<style lang="stylus"> <style lang="stylus">
@import "@/assets/css/image-mj.styl" @import '@/assets/css/image-mj.styl';
@import "@/assets/css/custom-scroll.styl" @import '@/assets/css/custom-scroll.styl';
</style> </style>

View File

@ -12,9 +12,18 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.sampler" style="width: 150px"> <el-select v-model="params.sampler" style="width: 150px">
<el-option v-for="item in samplers" :label="item" :value="item" :key="item" /> <el-option
v-for="item in samplers"
:label="item"
:value="item"
:key="item"
/>
</el-select> </el-select>
<el-tooltip content="出图效果比较好的一般是 Euler 和 DPM 系列算法" raw-content placement="right"> <el-tooltip
content="出图效果比较好的一般是 Euler 和 DPM 系列算法"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -28,10 +37,22 @@
<el-form-item label="采样调度"> <el-form-item label="采样调度">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.scheduler" style="width: 150px"> <el-select
<el-option v-for="item in schedulers" :label="item" :value="item" :key="item" /> v-model="params.scheduler"
style="width: 150px"
>
<el-option
v-for="item in schedulers"
:label="item"
:value="item"
:key="item"
/>
</el-select> </el-select>
<el-tooltip content="推荐自动或者 Karras" raw-content placement="right"> <el-tooltip
content="推荐自动或者 Karras"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -47,10 +68,16 @@
<div class="form-item-inner"> <div class="form-item-inner">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-input v-model.number="params.width" placeholder="图片宽度" /> <el-input
v-model.number="params.width"
placeholder="图片宽度"
/>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-input v-model.number="params.height" placeholder="图片高度" /> <el-input
v-model.number="params.height"
placeholder="图片高度"
/>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@ -63,7 +90,11 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-input v-model.number="params.steps" /> <el-input v-model.number="params.steps" />
<el-tooltip content="值越大则代表细节越多,同时也意味着出图速度越慢" raw-content placement="right"> <el-tooltip
content="值越大则代表细节越多,同时也意味着出图速度越慢"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -78,7 +109,11 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-input v-model.number="params.cfg_scale" /> <el-input v-model.number="params.cfg_scale" />
<el-tooltip content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果" raw-content placement="right"> <el-tooltip
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -93,13 +128,21 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-input v-model.number="params.seed" /> <el-input v-model.number="params.seed" />
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right"> <el-tooltip
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
<el-tooltip content="使用随机数" raw-content placement="right"> <el-tooltip
content="使用随机数"
raw-content
placement="right"
>
<el-icon @click="params.seed = -1" class="info-icon"> <el-icon @click="params.seed = -1" class="info-icon">
<Orange /> <Orange />
</el-icon> </el-icon>
@ -113,8 +156,16 @@
<el-form-item label="高清修复"> <el-form-item label="高清修复">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1" size="large" /> <el-switch
<el-tooltip content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节" raw-content placement="right"> v-model="params.hd_fix"
style="--el-switch-on-color: #47fff1"
size="large"
/>
<el-tooltip
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
raw-content
placement="right"
>
<el-icon style="margin-left: 10px; top: 12px"> <el-icon style="margin-left: 10px; top: 12px">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -129,8 +180,20 @@
<el-form-item label="重绘幅度"> <el-form-item label="重绘幅度">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" style="width: 180px; --el-slider-main-bg-color: #47fff1" /> <el-slider
<el-tooltip content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像" raw-content placement="right"> v-model.number="params.hd_redraw_rate"
:max="1"
:step="0.1"
style="
width: 180px;
--el-slider-main-bg-color: #47fff1;
"
/>
<el-tooltip
content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -144,10 +207,22 @@
<el-form-item label="放大算法"> <el-form-item label="放大算法">
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-select v-model="params.hd_scale_alg" style="width: 176px"> <el-select
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item" /> v-model="params.hd_scale_alg"
style="width: 176px"
>
<el-option
v-for="item in scaleAlg"
:label="item"
:value="item"
:key="item"
/>
</el-select> </el-select>
<el-tooltip content="高清修复放大算法主流算法有Latent和ESRGAN_4x" raw-content placement="right"> <el-tooltip
content="高清修复放大算法主流算法有Latent和ESRGAN_4x"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -162,7 +237,11 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-input v-model.number="params.hd_scale" /> <el-input v-model.number="params.hd_scale" />
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right"> <el-tooltip
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -177,7 +256,11 @@
<template #default> <template #default>
<div class="form-item-inner"> <div class="form-item-inner">
<el-input v-model.number="params.hd_steps" /> <el-input v-model.number="params.hd_steps" />
<el-tooltip content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数" raw-content placement="right"> <el-tooltip
content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数"
raw-content
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
@ -201,22 +284,39 @@
</div> </div>
<el-row class="text-info"> <el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating"> <el-button
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i> class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i
class="iconfont icon-chuangzuo"
style="margin-right: 5px"
></i>
<span>生成专业绘画指令</span> <span>生成专业绘画指令</span>
</el-button> </el-button>
</el-row> </el-row>
<div class="param-line pt"> <div class="param-line pt">
<span>反向提示词</span> <span>反向提示词</span>
<el-tooltip content="不希望出现的元素,下面给了默认的起手式" placement="right"> <el-tooltip
content="不希望出现的元素,下面给了默认的起手式"
placement="right"
>
<el-icon class="info-icon"> <el-icon class="info-icon">
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="param-line"> <div class="param-line">
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea" placeholder="反向提示词" /> <el-input
v-model="params.neg_prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
placeholder="反向提示词"
/>
</div> </div>
<div class="text-info"> <div class="text-info">
@ -226,80 +326,183 @@
<el-text type="warning">{{ sdPower }}算力</el-text> <el-text type="warning">{{ sdPower }}算力</el-text>
</el-text> </el-text>
<el-text type="primary" <el-text type="primary"
>当前可用 <el-text type="warning"> {{ power }}算力</el-text></el-text >当前可用
<el-text type="warning"> {{ power }}算力</el-text></el-text
> >
</el-row> </el-row>
</div> </div>
</el-form> </el-form>
</div> </div>
<div class="submit-btn"> <div class="submit-btn">
<el-button type="primary" :dark="false" round @click="generate">立即生成</el-button> <el-button type="primary" :dark="false" round @click="generate"
>立即生成</el-button
>
</div> </div>
</div> </div>
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh"> <div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }"> <div
class="task-list-inner"
:style="{ height: listBoxHeight + 'px' }"
>
<div class="job-list-box"> <div class="job-list-box">
<h2 class="text-xl">任务列表</h2> <h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" /> <task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0"> <template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2> <h2 class="text-xl">创作记录</h2>
<div class="finish-job-list"> <div class="finish-job-list mt-3">
<div v-if="finishedJobs.length > 0"> <div v-if="finishedJobs.length > 0">
<v3-waterfall <Waterfall
id="waterfall"
:list="finishedJobs" :list="finishedJobs"
srcKey="img_thumb" :row-key="waterfallOptions.rowKey"
:gap="20" :gutter="waterfallOptions.gutter"
:bottomGap="-10" :has-around-gutter="waterfallOptions.hasAroundGutter"
:colWidth="colWidth" :width="waterfallOptions.width"
:distanceToScroll="100" :breakpoints="waterfallOptions.breakpoints"
:isLoading="loading" :img-selector="waterfallOptions.imgSelector"
:isOver="isOver" :background-color="waterfallOptions.backgroundColor"
@scrollReachBottom="fetchFinishJobs()" :animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
> >
<template #default="slotProp"> <template #default="{ item, url }">
<div class="job-item animate"> <div
<el-image v-if="slotProp.item.progress === 101"> class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
<template #error> >
<div class="image-slot"> <div class="overflow-hidden rounded-lg">
<div class="err-msg-container"> <LazyImg
<div class="title">任务失败</div> :url="url"
<div class="opt"> v-if="item.progress === 100"
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top"> class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
<template #reference> @click="showTask(item)"
<el-button type="info">详情</el-button> />
</template> <el-image v-else-if="item.progress === 101">
</el-popover> <template #error>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button> <div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="item['err_msg']"
placement="top"
>
<template #reference>
<el-button type="info"
>详情</el-button
>
</template>
</el-popover>
<el-button
type="danger"
@click="removeImage(item)"
>删除</el-button
>
</div>
</div> </div>
</div> </div>
</template>
</el-image>
</div>
<div
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
v-if="item.progress === 100"
>
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="flex">
<el-tooltip
content="取消分享"
placement="top"
v-if="item.publish"
>
<el-button
type="warning"
@click="publishImage(item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip
content="分享"
placement="top"
v-else
>
<el-button
type="success"
@click="publishImage(item, true)"
circle
>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(item)"
circle
/>
</el-tooltip>
</div> </div>
</template>
</el-image>
<div v-else>
<el-image :src="slotProp.item['img_thumb']" @click="showTask(slotProp.item)" fit="cover" loading="lazy" />
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
<el-button type="warning" v-if="slotProp.item.publish" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</Waterfall>
<template #footer> <div class="flex justify-center py-10">
<div class="no-more-data"> <img
<span>没有更多数据了</span> :src="waterfallOptions.loadProps.loading"
class="max-w-[50px] max-h-[50px]"
v-if="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="fetchFinishJobs"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i> <i class="iconfont icon-face"></i>
</div> </div>
</template> </div>
</v3-waterfall> </div>
</div> </div>
<el-empty :image-size="100" v-else :image="nodata" description="暂无记录" /> <el-empty
:image-size="100"
v-else
:image="nodata"
description="暂无记录"
/>
</div> </div>
</template> </template>
@ -312,14 +515,24 @@
</div> </div>
<!-- 任务详情弹框 --> <!-- 任务详情弹框 -->
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="copyParams" @close="showTaskDialog = false" /> <sd-task-view
v-model="showTaskDialog"
:data="item"
@drawSame="copyParams"
@close="showTaskDialog = false"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue"; import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { Delete, DocumentCopy, InfoFilled, Orange } from "@element-plus/icons-vue"; import {
Delete,
DocumentCopy,
InfoFilled,
Orange,
} from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png"; import nodata from "@/assets/img/no-data.png";
import { httpGet, httpPost } from "@/utils/http"; import { httpGet, httpPost } from "@/utils/http";
@ -333,15 +546,17 @@ import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import { showMessageError } from "@/utils/dialog"; import { showMessageError } from "@/utils/dialog";
import SdTaskView from "@/components/SdTaskView.vue"; import SdTaskView from "@/components/SdTaskView.vue";
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import "vue-waterfall-plugin-next/dist/style.css";
const listBoxHeight = ref(0); const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0) // const paramBoxHeight = ref(0)
const fullImgHeight = ref(window.innerHeight - 60);
const showTaskDialog = ref(false); const showTaskDialog = ref(false);
const item = ref({}); const item = ref({});
const isLogin = ref(false); const isLogin = ref(false);
const loading = ref(true); const loading = ref(true);
const colWidth = ref(220);
const store = useSharedStore(); const store = useSharedStore();
const waterfallOptions = store.waterfallOptions;
const resizeElement = function () { const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 80; listBoxHeight.value = window.innerHeight - 80;
@ -351,7 +566,15 @@ resizeElement();
window.onresize = () => { window.onresize = () => {
resizeElement(); resizeElement();
}; };
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"]; const samplers = [
"Euler a",
"DPM++ 2S a",
"DPM++ 2M",
"DPM++ SDE",
"DPM++ 2M SDE",
"UniPC",
"Restart",
];
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"]; const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"];
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]; const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"];
const params = ref({ const params = ref({
@ -368,7 +591,8 @@ const params = ref({
hd_scale_alg: scaleAlg[0], hd_scale_alg: scaleAlg[0],
hd_steps: 0, hd_steps: 0,
prompt: "", prompt: "",
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet", neg_prompt:
"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
}); });
const runningJobs = ref([]); const runningJobs = ref([]);
@ -466,25 +690,28 @@ const fetchFinishJobs = () => {
loading.value = true; loading.value = true;
page.value = page.value + 1; page.value = page.value + 1;
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`) httpGet(
`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`
)
.then((res) => { .then((res) => {
if (res.data.items.length < pageSize.value) { if (res.data.items.length < pageSize.value) {
isOver.value = true; isOver.value = true;
loading.value = false;
} }
const imageList = res.data.items; const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) { for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"; imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
} }
if (page.value === 1) { if (page.value === 1) {
finishedJobs.value = imageList; finishedJobs.value = imageList;
} else { } else {
finishedJobs.value = finishedJobs.value.concat(imageList); finishedJobs.value = finishedJobs.value.concat(imageList);
} }
loading.value = false;
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取任务失败:" + e.message); ElMessage.error("获取任务失败:" + e.message);
loading.value = false;
}); });
}; };
@ -588,6 +815,6 @@ const generatePrompt = () => {
</script> </script>
<style lang="stylus"> <style lang="stylus">
@import "@/assets/css/image-sd.styl" @import '@/assets/css/image-sd.styl';
@import "@/assets/css/custom-scroll.styl" @import '@/assets/css/custom-scroll.styl';
</style> </style>

View File

@ -11,149 +11,238 @@
</el-radio-group> </el-radio-group>
</div> </div>
</div> </div>
<div class="waterfall" :style="{ height: listBoxHeight + 'px' }" id="waterfall-box"> <div
<v3-waterfall class="waterfall"
:style="{ height: listBoxHeight + 'px' }"
id="waterfall-box"
>
<Waterfall
v-if="imgType === 'mj'" v-if="imgType === 'mj'"
id="waterfall" id="waterfall-mj"
:list="data['mj']" :list="data['mj']"
srcKey="img_thumb" :row-key="waterfallOptions.rowKey"
:gap="12" :gutter="waterfallOptions.gutter"
:bottomGap="-5" :has-around-gutter="waterfallOptions.hasAroundGutter"
:colWidth="colWidth" :width="waterfallOptions.width"
:distanceToScroll="100" :breakpoints="waterfallOptions.breakpoints"
:isLoading="loading" :img-selector="waterfallOptions.imgSelector"
:isOver="isOver" :background-color="waterfallOptions.backgroundColor"
@scrollReachBottom="getNext" :animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
> >
<template #default="slotProp"> <template #default="{ item, url }">
<div class="list-item"> <div
<div class="image"> class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
<el-image >
:src="slotProp.item['img_thumb']" <div class="overflow-hidden rounded-lg">
:zoom-rate="1.2" <LazyImg
:preview-src-list="[slotProp.item['img_url']]" :url="url"
:preview-teleported="true" class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
:initial-index="10" @click="previewImg(item)"
loading="lazy" />
>
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div> </div>
<div class="opt"> <div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<el-tooltip class="box-item" content="复制提示词" placement="top"> <div
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt"> class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
<DocumentCopy /> >
</el-icon> <div class="opt">
</el-tooltip> <el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip class="box-item" content="画同款" placement="top"> <el-tooltip
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i> class="box-item"
</el-tooltip> content="画同款"
placement="top"
>
<el-button
type="primary"
circle
@click="drawSameMj(item)"
>
<i class="iconfont icon-palette"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
</v3-waterfall> </Waterfall>
<v3-waterfall <Waterfall
v-else-if="imgType === 'dall'" v-if="imgType === 'sd'"
id="waterfall" id="waterfall-sd"
:list="data['dall']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="getNext"
>
<template #default="slotProp">
<div class="list-item">
<div class="image">
<el-image
:src="slotProp.item['img_thumb']"
:zoom-rate="1.2"
:preview-src-list="[slotProp.item['img_url']]"
:preview-teleported="true"
:initial-index="10"
loading="lazy"
>
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div class="opt">
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy />
</el-icon>
</el-tooltip>
</div>
</div>
</template>
</v3-waterfall>
<v3-waterfall
v-else
id="waterfall"
:list="data['sd']" :list="data['sd']"
srcKey="img_thumb" :row-key="waterfallOptions.rowKey"
:gap="12" :gutter="waterfallOptions.gutter"
:bottomGap="-5" :has-around-gutter="waterfallOptions.hasAroundGutter"
:colWidth="colWidth" :width="waterfallOptions.width"
:distanceToScroll="100" :breakpoints="waterfallOptions.breakpoints"
:isLoading="loading" :img-selector="waterfallOptions.imgSelector"
:isOver="isOver" :background-color="waterfallOptions.backgroundColor"
@scrollReachBottom="getNext" :animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
> >
<template #default="slotProp"> <template #default="{ item, url }">
<div class="list-item"> <div
<div class="image"> class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
<el-image :src="slotProp.item['img_thumb']" loading="lazy" @click="showTask(slotProp.item)"> >
<template #placeholder> <div class="overflow-hidden rounded-lg">
<div class="image-slot">正在加载图片</div> <LazyImg
</template> :url="url"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="showTask(item)"
/>
</div>
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<template #error> <el-tooltip
<div class="image-slot"> class="box-item"
<el-icon> content="画同款"
<Picture /> placement="top"
</el-icon> >
</div> <el-button
</template> type="primary"
</el-image> circle
@click="drawSameSd(item)"
>
<i class="iconfont icon-palette"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
</v3-waterfall> </Waterfall>
<div class="footer" v-if="isOver"> <Waterfall
<!-- <el-empty v-if="imgType === 'dall'"
:image-size="100" id="waterfall-dall"
:image="nodata" :list="data['dall']"
description="没有更多数据了" :row-key="waterfallOptions.rowKey"
/> --> :gutter="waterfallOptions.gutter"
<span>没有更多数据了</span> :has-around-gutter="waterfallOptions.hasAroundGutter"
<i class="iconfont icon-face"></i> :width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="previewImg(item)"
/>
</div>
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
</Waterfall>
<div class="flex flex-col items-center justify-center py-10">
<img
:src="waterfallOptions.loadProps.loading"
class="max-w-[50px] max-h-[50px]"
v-if="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="getNext"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</div>
</div> </div>
<back-top :right="30" :bottom="30" /> <back-top :right="30" :bottom="30" />
@ -161,7 +250,23 @@
<!-- end of waterfall --> <!-- end of waterfall -->
</div> </div>
<!-- 任务详情弹框 --> <!-- 任务详情弹框 -->
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="drawSameSd" @close="showTaskDialog = false" /> <sd-task-view
v-model="showTaskDialog"
:data="item"
@drawSame="drawSameSd"
@close="showTaskDialog = false"
/>
<!-- 图片预览 -->
<el-image-viewer
@close="
() => {
previewURL = '';
}
"
v-if="previewURL !== ''"
:url-list="[previewURL]"
/>
</div> </div>
</template> </template>
@ -175,6 +280,13 @@ import Clipboard from "clipboard";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import SdTaskView from "@/components/SdTaskView.vue"; import SdTaskView from "@/components/SdTaskView.vue";
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import "vue-waterfall-plugin-next/dist/style.css";
import { useSharedStore } from "@/store/sharedata";
const store = useSharedStore();
const waterfallOptions = store.waterfallOptions;
const data = ref({ const data = ref({
mj: [], mj: [],
sd: [], sd: [],
@ -184,19 +296,12 @@ const loading = ref(true);
const isOver = ref(false); const isOver = ref(false);
const imgType = ref("mj"); // const imgType = ref("mj"); //
const listBoxHeight = window.innerHeight - 124; const listBoxHeight = window.innerHeight - 124;
const colWidth = ref(220);
const showTaskDialog = ref(false); const showTaskDialog = ref(false);
const item = ref({}); const item = ref({});
const previewURL = ref("");
// const previewImg = (item) => {
const calcColWidth = () => { previewURL.value = item.img_url;
const listBoxWidth = window.innerWidth - 60 - 80;
const rows = Math.floor(listBoxWidth / colWidth.value);
colWidth.value = Math.floor((listBoxWidth - (rows - 1) * 12) / rows);
};
calcColWidth();
window.onresize = () => {
calcColWidth();
}; };
const page = ref(0); const page = ref(0);
@ -223,16 +328,17 @@ const getNext = () => {
} }
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`) httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
.then((res) => { .then((res) => {
loading.value = false;
if (!res.data.items || res.data.items.length === 0) { if (!res.data.items || res.data.items.length === 0) {
isOver.value = true; isOver.value = true;
loading.value = false;
return; return;
} }
// //
const imageList = res.data.items; const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) { for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"; imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
} }
if (data.value[imgType.value].length === 0) { if (data.value[imgType.value].length === 0) {
data.value[imgType.value] = imageList; data.value[imgType.value] = imageList;
@ -246,6 +352,7 @@ const getNext = () => {
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取图片失败:" + e.message); ElMessage.error("获取图片失败:" + e.message);
loading.value = false;
}); });
}; };
@ -300,6 +407,6 @@ const drawSameMj = (row) => {
</script> </script>
<style lang="stylus"> <style lang="stylus">
@import "@/assets/css/images-wall.styl" @import '@/assets/css/images-wall.styl';
@import "@/assets/css/custom-scroll.styl" @import '@/assets/css/custom-scroll.styl';
</style> </style>