geekai/web/src/views/Dalle.vue
2025-04-03 16:48:56 +08:00

619 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<div class="page-dall">
<div class="inner custom-scroll">
<div class="sd-box">
<h2>DALL-E 创作中心</h2>
<div class="sd-params">
<el-form :model="params" label-width="80px" label-position="left">
<div class="param-line pt-1">
<el-form-item label="生图模型">
<template #default>
<div class="form-item-inner">
<el-select
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>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="图片质量">
<template #default>
<div class="form-item-inner">
<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-select>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="图片尺寸">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.size" style="width: 150px">
<el-option
v-for="v in sizes"
:label="v"
:value="v"
:key="v"
/>
</el-select>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="图片样式">
<template #default>
<div class="form-item-inner">
<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-select>
<el-tooltip
content="生动使模型倾向于生成超真实和戏剧性的图像"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-input
v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
maxlength="2000"
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
v-loading="isGenerating"
/>
</div>
<el-row class="text-info">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i
class="iconfont icon-chuangzuo"
style="margin-right: 5px"
></i>
<span>生成专业绘画指令</span>
</el-button>
</el-row>
<div class="text-info">
<el-row :gutter="10">
<el-text type="primary"
>每次绘图消耗
<el-text type="warning"
>{{ dallPower }}算力</el-text
></el-text
>
<el-text type="primary"
>当前可用
<el-text type="warning"> {{ power }}算力</el-text>
</el-text>
</el-row>
</div>
</el-form>
</div>
<div class="submit-btn">
<el-button type="primary" :dark="false" round @click="generate">
立即生成
</el-button>
</div>
</div>
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
<div class="task-list-inner">
<div class="job-list-box">
<h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list mt-3">
<div v-if="finishedJobs.length > 0">
<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="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>
</div>
<el-empty
:image-size="100"
:image="nodata"
description="暂无记录"
v-else
/>
</div>
</template>
<!-- end finish job list-->
</div>
</div>
<back-top :right="30" :bottom="30" />
</div>
<!-- end task list box -->
</div>
</div>
<el-image-viewer
@close="
() => {
previewURL = '';
}
"
v-if="previewURL !== ''"
:url-list="[previewURL]"
/>
</div>
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { Delete, InfoFilled, Picture } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import "vue-waterfall-plugin-next/dist/style.css";
const listBoxHeight = ref(0);
// const paramBoxHeight = ref(0)
const isLogin = ref(false);
const loading = ref(true);
const isOver = ref(false);
const previewURL = ref("");
const store = useSharedStore();
const models = ref([]);
const waterfallOptions = store.waterfallOptions;
const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 58;
};
resizeElement();
window.onresize = () => {
resizeElement();
};
const qualities = [
{ name: "标准", value: "standard" },
{ name: "高清", value: "hd" },
];
const dalleSizes = ["1024x1024", "1792x1024", "1024x1792"];
const fluxSizes = [
"1024x1024",
"1024x768",
"768x1024",
"1280x960",
"960x1280",
"1366x768",
"768x1366",
];
const sizes = ref(dalleSizes);
const styles = [
{ name: "生动", value: "vivid" },
{ name: "自然", value: "natural" },
];
const params = ref({
quality: "standard",
size: "1024x1024",
style: "vivid",
prompt: "",
});
const finishedJobs = ref([]);
const runningJobs = ref([]);
const allowPulling = ref(true); // 是否允许轮询
const tastPullHandler = ref(null);
const power = ref(0);
const dallPower = ref(0); // 画一张 SD 图片消耗算力
const clipboard = ref(null);
const userId = ref(0);
const selectedModel = ref(null);
onMounted(() => {
initData();
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on("success", () => {
showMessageOK("复制成功!");
});
clipboard.value.on("error", () => {
showMessageError("复制失败!");
});
getSystemInfo()
.then((res) => {
dallPower.value = res.data["dall_power"];
})
.catch((e) => {
showMessageError("获取系统配置失败:" + e.message);
});
// 获取模型列表
httpGet("/api/dall/models")
.then((res) => {
models.value = res.data;
selectedModel.value = models.value[0];
params.value.model_id = selectedModel.value.id;
changeModel(selectedModel.value);
})
.catch((e) => {
showMessageError("获取模型列表失败:" + e.message);
});
});
onUnmounted(() => {
clipboard.value.destroy();
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
}
});
const initData = () => {
checkSession()
.then((user) => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
// 轮询运行中任务
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
}
}, 5000);
})
.catch(() => {});
};
const fetchRunningJobs = () => {
if (!isLogin.value) {
return;
}
// 获取运行中的任务
httpGet(`/api/dall/jobs?finish=false`)
.then((res) => {
// 如果任务有更新,则更新已完成任务列表
if (
res.data.items &&
res.data.items.length !== runningJobs.value.length
) {
page.value = 0;
fetchFinishJobs();
}
if (res.data.items.length > 0) {
runningJobs.value = res.data.items;
} else {
allowPulling.value = false;
runningJobs.value = [];
}
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
const page = ref(1);
const pageSize = ref(15);
// 获取已完成的任务
const fetchFinishJobs = () => {
if (!isLogin.value) {
return;
}
loading.value = true;
page.value = page.value + 1;
httpGet(
`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`
)
.then((res) => {
if (res.data.items.length < pageSize.value) {
isOver.value = true;
loading.value = false;
}
const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
}
if (page.value === 1) {
finishedJobs.value = imageList;
} else {
finishedJobs.value = finishedJobs.value.concat(imageList);
}
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
loading.value = false;
});
};
// 创建绘图任务
const promptRef = ref(null);
const generate = () => {
if (params.value.prompt === "") {
promptRef.value.focus();
return ElMessage.error("请输入绘画提示词!");
}
if (!isLogin.value) {
store.setShowLoginDialog(true);
return;
}
httpPost("/api/dall/image", params.value)
.then(() => {
ElMessage.success("任务执行成功!");
power.value -= dallPower.value;
// 追加任务列表
runningJobs.value.push({
prompt: params.value.prompt,
progress: 0,
});
allowPulling.value = true;
})
.catch((e) => {
ElMessage.error("任务执行失败:" + e.message);
});
};
const removeImage = (item) => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
httpGet("/api/dall/remove", { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
page.value = 0;
isOver.value = false;
fetchFinishJobs();
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
})
.catch(() => {});
};
const previewImg = (item) => {
previewURL.value = item.img_url;
};
// 发布图片到作品墙
const publishImage = (item, action) => {
let text = "图片发布";
if (action === false) {
text = "取消发布";
}
httpGet("/api/dall/publish", { id: item.id, action: action })
.then(() => {
ElMessage.success(text + "成功");
item.publish = action;
page.value = 0;
isOver.value = false;
})
.catch((e) => {
ElMessage.error(text + "失败:" + e.message);
});
};
const isGenerating = ref(false);
const generatePrompt = () => {
if (params.value.prompt === "") {
return showMessageError("请输入原始提示词");
}
isGenerating.value = true;
httpPost("/api/prompt/image", { prompt: params.value.prompt })
.then((res) => {
params.value.prompt = res.data;
isGenerating.value = false;
})
.catch((e) => {
showMessageError("生成提示词失败:" + e.message);
isGenerating.value = false;
});
};
const changeModel = (model) => {
if (model.value.startsWith("dall")) {
sizes.value = dalleSizes;
} else {
sizes.value = fluxSizes;
}
params.value.model_id = selectedModel.value.id;
};
</script>
<style lang="stylus">
@import '@/assets/css/image-dall.styl';
@import '@/assets/css/custom-scroll.styl';
</style>