mirror of
https://github.com/xiaoyiweb/YiAi.git
synced 2025-11-21 16:36:46 +08:00
2.5版本 增加dall-e 优化mj 对接mj-plus
This commit is contained in:
@@ -6,20 +6,23 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
gap: 10,
|
||||
progress: 0,
|
||||
tips: '',
|
||||
words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
|
||||
// words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
|
||||
words: ['AI', '绘', '画', '中'],
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const theme = computed(() => appStore.theme)
|
||||
const loadingTextColor = computed(() => theme.value === 'dark' ? '#fff' : '#000')
|
||||
const loadingTextColor = computed(() =>
|
||||
theme.value === 'dark' ? '#fff' : '#000'
|
||||
)
|
||||
|
||||
interface Props {
|
||||
gap?: number
|
||||
progress?: number
|
||||
tips?: string
|
||||
bgColor?: string
|
||||
words?: any
|
||||
words?: any
|
||||
}
|
||||
// const words = ref<string[]>(['L', 'O', 'A', 'D', 'I', 'N', 'G'])
|
||||
</script>
|
||||
@@ -27,7 +30,13 @@ interface Props {
|
||||
<template>
|
||||
<div class="loading" :style="{ background: props.bgColor }">
|
||||
<div class="loading-text">
|
||||
<span v-for="item in props.words" :key="item" :style="{ margin: `0 ${props.gap}px`, color: loadingTextColor }" class="loading-text-words">{{ item }}</span>
|
||||
<span
|
||||
v-for="item in props.words"
|
||||
:key="item"
|
||||
:style="{ margin: `0 ${props.gap}px`, color: loadingTextColor }"
|
||||
class="loading-text-words"
|
||||
>{{ item }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="!tips && props.progress" class="progress">
|
||||
绘制进度: {{ props.progress }}%
|
||||
@@ -46,14 +55,14 @@ interface Props {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.progress{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
@@ -72,42 +81,42 @@ interface Props {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
color: #fff;
|
||||
font-family: "Quattrocento Sans", sans-serif;
|
||||
font-family: 'Quattrocento Sans', sans-serif;
|
||||
}
|
||||
.loading-text span:nth-child(1) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(2) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.2s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.2s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.2s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(3) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.4s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.4s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.4s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(4) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.6s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.6s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.6s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(5) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.8s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.8s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.8s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(6) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 1s infinite linear alternate;
|
||||
animation: blur-text 1.5s 1s infinite linear alternate;
|
||||
animation: blur-text 1.5s 1s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(7) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 1.2s infinite linear alternate;
|
||||
animation: blur-text 1.5s 1.2s infinite linear alternate;
|
||||
animation: blur-text 1.5s 1.2s infinite linear alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes blur-text {
|
||||
|
||||
377
chat/src/components/common/GridManager2/index.vue
Normal file
377
chat/src/components/common/GridManager2/index.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
onMounted,
|
||||
ref,
|
||||
computed,
|
||||
onUnmounted,
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
nextTick,
|
||||
} from 'vue';
|
||||
import { throttle } from '@/utils/functions/throttle';
|
||||
import { SvgIcon } from '@/components/common';
|
||||
import { copyText } from '@/utils/format';
|
||||
import { useMessage, NPopover } from 'naive-ui';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { useRouter } from 'vue-router';
|
||||
const authStore = useAuthStore();
|
||||
interface Props {
|
||||
dataList: FileItem[];
|
||||
scaleWidth?: number;
|
||||
isDrawLike?: boolean;
|
||||
usePropmpt?: boolean;
|
||||
copyPropmpt?: boolean;
|
||||
gap?: number;
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
id: number;
|
||||
drawUrl: string;
|
||||
fullPrompt?: string;
|
||||
drawRatio: string;
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(ev: 'loadMore'): void;
|
||||
(ev: 'usePropmptDraw', prompt: string): void;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gap: 5,
|
||||
});
|
||||
const emit = defineEmits<Emit>();
|
||||
const $viewerApi =
|
||||
getCurrentInstance()?.appContext.config.globalProperties.$viewerApi;
|
||||
const ms = useMessage();
|
||||
const boxRefs = ref<any>({});
|
||||
const otherInfoContainerHeight = ref(0);
|
||||
const realWidth = ref(160);
|
||||
const realColumn = ref(0);
|
||||
const loadComplete = ref<number[]>([]);
|
||||
const wapperRef = ref<HTMLDivElement | null>(null);
|
||||
const wapperHeigth = ref(0);
|
||||
const isLogin = computed(() => authStore.isLogin);
|
||||
const width = computed(() => {
|
||||
return props.scaleWidth
|
||||
? Number(props.scaleWidth) * 2 + props.gap + 150
|
||||
: 150;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
/* 拿到图片高度 对定位top和right 新的一轮去插入最小值的那一列 贪心算法即可 */
|
||||
function compilerContainer() {
|
||||
calcHeight();
|
||||
compilerColumn();
|
||||
const columns = realColumn.value;
|
||||
const itemWidth = realWidth.value;
|
||||
const cacheHeight = <any>[];
|
||||
props.dataList.forEach((item, index) => {
|
||||
const drawRatio = item.drawRatio; // 假设 drawRatio 是 "1632x2912"
|
||||
const dimensions = drawRatio.split('x'); // 使用 'x' 分割字符串
|
||||
const width = parseInt(dimensions[0], 10); // 宽度,转换为数字
|
||||
const height = parseInt(dimensions[1], 10); // 高度,转换为数字
|
||||
const bi = itemWidth / width;
|
||||
const boxheight = height * bi + props.gap + otherInfoContainerHeight.value;
|
||||
const currentBox = boxRefs.value[item.id];
|
||||
if (cacheHeight.length < columns) {
|
||||
currentBox.style.top = '0px';
|
||||
currentBox.style.left = `${(itemWidth + props.gap) * index}px`;
|
||||
cacheHeight.push(boxheight);
|
||||
} else {
|
||||
const minHeight = Math.min.apply(null, cacheHeight);
|
||||
const minIndex = cacheHeight.findIndex((t: number) => t === minHeight);
|
||||
currentBox.style.top = `${minHeight + 0}px`;
|
||||
currentBox.style.left = `${minIndex * (realWidth.value + props.gap)}px`;
|
||||
cacheHeight[minIndex] += boxheight;
|
||||
}
|
||||
});
|
||||
wapperHeigth.value = Math.max(...cacheHeight) + 100;
|
||||
}
|
||||
|
||||
function setItemRefs(el: HTMLDivElement, item: FileItem) {
|
||||
if (el && item) {
|
||||
boxRefs.value[item.id] = el;
|
||||
}
|
||||
}
|
||||
|
||||
/* 通过额外展示的信息计算有没有除了图片意外额外的高度 eg: 图片100px 额外显示其他信息30px cacheHeight的高度在图片的基础上需要+30 */
|
||||
function calcHeight() {
|
||||
const { showName = 0, showOther = 0 } = {};
|
||||
otherInfoContainerHeight.value =
|
||||
[showName, showOther].filter((t) => t).length * 15;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.scaleWidth,
|
||||
(val) => {
|
||||
handleResizeThrottled();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.dataList,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
nextTick(() => {
|
||||
handleResizeThrottled();
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/* 计算放多少列比较合理,并计算最终单个图片的宽 */
|
||||
function compilerColumn() {
|
||||
if (!wapperRef.value) return;
|
||||
const containerWidth = wapperRef.value.clientWidth;
|
||||
|
||||
/* 计算按目前宽度最多可以是几列 */
|
||||
realColumn.value = Math.floor(containerWidth / width.value);
|
||||
const surplus = containerWidth - realColumn.value * width.value; // 剩下的多余空间
|
||||
/* 计算如果给了左右间距那么作业间距需要占多少宽度 */
|
||||
const positionWith = (realColumn.value - 1) * props.gap; // 设置的right 需要padding的值
|
||||
/* 总宽度减去right的宽度,如果是负数考虑要不要cloumn-1 那么图片真实宽度就会比传入的宽度大 */
|
||||
if (surplus - positionWith < 0) {
|
||||
realColumn.value -= 1;
|
||||
}
|
||||
/* 图片宽度*列 + right的间距 不管大于小于总宽 多的或者少的那部分都平分给列容器 保证总宽是100% */
|
||||
realWidth.value = Math.floor(
|
||||
(containerWidth - positionWith) / realColumn.value
|
||||
);
|
||||
}
|
||||
|
||||
function imgLoadSuccess(e: any, item: FileItem) {
|
||||
loadComplete.value.push(item.id);
|
||||
}
|
||||
|
||||
function imgLoadError(e: any, item: FileItem) {
|
||||
console.error('Image failed to load:', item);
|
||||
loadComplete.value.push(item.id);
|
||||
}
|
||||
|
||||
function handleCopy(item: any) {
|
||||
if (!isLogin.value) {
|
||||
return authStore.setLoginDialog(true);
|
||||
}
|
||||
const { prompt } = item;
|
||||
copyText({ text: prompt });
|
||||
ms.success('复制prompt成功');
|
||||
}
|
||||
|
||||
function drawLike(item: any) {
|
||||
router.push(`/midjourney?mjId=${item.id}`);
|
||||
}
|
||||
|
||||
function usePropmptDraw(item: FileItem) {
|
||||
const { fullPrompt } = item;
|
||||
emit('usePropmptDraw', fullPrompt);
|
||||
}
|
||||
|
||||
function handlePreview(item: any) {
|
||||
const { drawUrl } = item;
|
||||
$viewerApi({ options: {}, images: [drawUrl] });
|
||||
}
|
||||
|
||||
const realHeight = computed(() => (item) => {
|
||||
const ratio = item.drawRatio.split('x');
|
||||
const originalWidth = Number(ratio[0]);
|
||||
const originalHeight = Number(ratio[1]);
|
||||
return originalHeight / (originalWidth / realWidth.value);
|
||||
});
|
||||
|
||||
const handleResizeThrottled = throttle(function (this: any) {
|
||||
compilerContainer();
|
||||
}, 200);
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResizeThrottled);
|
||||
const container: any = document.getElementById('footer');
|
||||
const observer = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
emit('loadMore');
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(container);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResizeThrottled);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full overflow-hidden flex flex-col">
|
||||
<div class="flex-1 min-h-full p-4 relative">
|
||||
<div
|
||||
id="wapper"
|
||||
ref="wapperRef"
|
||||
class="wapper"
|
||||
:style="{ height: `${wapperHeigth}px` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in dataList"
|
||||
:id="item.id.toString()"
|
||||
:key="index"
|
||||
:ref="(el) => setItemRefs(el, item)"
|
||||
class="wapper-item"
|
||||
:style="{ width: `${realWidth}px` }"
|
||||
>
|
||||
<transition name="img" :css="true">
|
||||
<img
|
||||
:id="item.id.toString()"
|
||||
class="item-file rounded-sm"
|
||||
:style="{
|
||||
width: `${realWidth}px`,
|
||||
height: `${realHeight(item)}px`,
|
||||
}"
|
||||
:src="item.drawUrl"
|
||||
loading="lazy"
|
||||
@load="imgLoadSuccess($event, item)"
|
||||
@error="imgLoadError($event, item)"
|
||||
@click="handlePreview(item)"
|
||||
/>
|
||||
</transition>
|
||||
<div class="menu p-2 text-[#cbd5e1]">
|
||||
<div class="prompt">
|
||||
{{ item.fullPrompt }}
|
||||
</div>
|
||||
<div class="flex justify-end items-end space-x-2">
|
||||
<n-popover trigger="hover" v-if="isDrawLike">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
|
||||
@click.stop="drawLike(item)"
|
||||
>
|
||||
<span class="text-sm dark:text-slate-400">
|
||||
<SvgIcon
|
||||
icon="fluent:draw-image-24-regular"
|
||||
class="text-sm"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span>画同款</span>
|
||||
</n-popover>
|
||||
|
||||
<n-popover trigger="hover" v-if="usePropmpt">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
|
||||
@click.stop="usePropmptDraw(item)"
|
||||
>
|
||||
<span class="text-sm dark:text-slate-400">
|
||||
<SvgIcon
|
||||
icon="fluent:draw-image-24-regular"
|
||||
class="text-sm"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span>使用当前画同款</span>
|
||||
</n-popover>
|
||||
|
||||
<n-popover trigger="hover" v-if="copyPropmpt">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
|
||||
@click.stop="handleCopy(item)"
|
||||
>
|
||||
<span class="text-sm dark:text-slate-400">
|
||||
<SvgIcon icon="tabler:copy" class="text-sm" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span>复制提示词</span>
|
||||
</n-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item-loading"
|
||||
v-if="!loadComplete.includes(item.id)"
|
||||
:style="{
|
||||
width: `${realWidth}px`,
|
||||
height: `${realHeight(item)}px`,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div id="footer" class="w-full absolute bottom-[350px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.wapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
|
||||
&-item {
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transition: all 0.5s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.menu {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 94%;
|
||||
left: 3%;
|
||||
max-height: 70%;
|
||||
height: 100px;
|
||||
transform: translateY(100%);
|
||||
background-color: #090b15;
|
||||
opacity: 0.8;
|
||||
transition: all 0.1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.prompt {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.6s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.item-loading {
|
||||
background: url(@/assets/img-bg.png) no-repeat center center;
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img-enter-active,
|
||||
.img-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.img-enter,
|
||||
.img-leave-to {
|
||||
transform: scale(0.6);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -317,7 +317,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.item-loading {
|
||||
background: url(../../assets/img-bg.png) no-repeat center center;
|
||||
background: url(@/assets/img-bg.png) no-repeat center center;
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
Reference in New Issue
Block a user