feat: 图生图功能
feat: 参考图片(图生图)选择器 ui: 文件上传(预留) composable ui: 登录过期提示 ui: 调整部分 ui
This commit is contained in:
127
components/aigc/ReferenceFigureSelector.vue
Normal file
127
components/aigc/ReferenceFigureSelector.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<File | null>,
|
||||
default: null,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '选择图片进行图生图',
|
||||
},
|
||||
textOnSelect: {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const selected_file = ref<File | null>(null)
|
||||
const image_dataurl = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
watch(() => props.value, async (newVal) => {
|
||||
handleFileInput({target: {files: [newVal!]}})
|
||||
})
|
||||
|
||||
const handleTrashClick = () => {
|
||||
fileInput.value!.value = '';
|
||||
selected_file.value = null
|
||||
image_dataurl.value = ''
|
||||
emit('update', null)
|
||||
}
|
||||
|
||||
const handleFileInput = (event: { target: any; }) => {
|
||||
if (event.target.files) {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
selected_file.value = file
|
||||
loading.value = true
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
image_dataurl.value = e.target?.result as string
|
||||
loading.value = false
|
||||
}
|
||||
reader.onerror = (e) => {
|
||||
loading.value = false
|
||||
}
|
||||
reader.readAsDataURL(selected_file.value!)
|
||||
emit('update', selected_file.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full bg-neutral-200/50 dark:bg-neutral-700/50 rounded-md flex justify-between items-center p-1.5 gap-2 relative
|
||||
hover:bg-neutral-200/80 hover:dark:bg-neutral-700/80 transition border dark:border-neutral-700 cursor-pointer"
|
||||
:class="{'cursor-pointer': !loading, 'cursor-not-allowed': loading}"
|
||||
@click="() => !loading && fileInput?.click()">
|
||||
<input ref="fileInput" type="file" class="hidden" @change="handleFileInput" accept="image/*"/>
|
||||
<Transition name="trash-btn" mode="out-in">
|
||||
<button type="button" @click.stop.prevent="handleTrashClick" v-if="!!selected_file"
|
||||
class="absolute -top-1 -right-1 bg-white dark:bg-black rounded-full p-1 shadow-lg border dark:border-neutral-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</Transition>
|
||||
<div class="w-12 h-12 rounded-md overflow-hidden">
|
||||
<Transition name="preview-swap" mode="out-in">
|
||||
<div v-if="loading"
|
||||
class="w-full h-full flex justify-center items-center rounded-md border-2 border-dashed border-neutral-400 dark:border-neutral-600 text-neutral-400 dark:text-neutral-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"/>
|
||||
<path fill="currentColor"
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z">
|
||||
<animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate"
|
||||
values="0 12 12;360 12 12"/>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else-if="!selected_file"
|
||||
class="w-full h-full flex justify-center items-center rounded-md border-2 border-dashed border-neutral-400 dark:border-neutral-600 text-neutral-400 dark:text-neutral-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 5v14m-7-7h14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<img v-else class="w-12 h-12 rounded-md object-cover" :src="image_dataurl" :key="selected_file.name"
|
||||
alt="Preview">
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="flex-1 flex justify-center">
|
||||
<p class="text-neutral-400/80 dark:text-neutral-500 text-sm font-medium select-none text-center">
|
||||
{{ selected_file ? textOnSelect : text }}
|
||||
<span v-if="selected_file && textOnSelect" class="block text-[10px] text-center">
|
||||
{{ selected_file?.name || textOnSelect }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.trash-btn-enter-active,
|
||||
.trash-btn-leave-active {
|
||||
@apply transition duration-200;
|
||||
}
|
||||
|
||||
.trash-btn-enter-from,
|
||||
.trash-btn-leave-to {
|
||||
@apply opacity-0 scale-75;
|
||||
}
|
||||
|
||||
.preview-swap-enter-active,
|
||||
.preview-swap-leave-active {
|
||||
@apply transition duration-200;
|
||||
}
|
||||
|
||||
.preview-swap-enter-from,
|
||||
.preview-swap-leave-to {
|
||||
@apply blur-sm;
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-neutral-50 dark:bg-neutral-900 px-1.5 py-1 rounded-lg flex flex-col gap-1 shadow">
|
||||
<div class="bg-neutral-50 dark:bg-neutral-900 px-1.5 py-1 rounded flex flex-col gap-1 shadow">
|
||||
<div class="flex items-center gap-1 text-sm">
|
||||
<UIcon v-if="icon" :name="icon" class="text-base inline-block"/>
|
||||
<div class="flex-1 flex items-center truncate whitespace-nowrap overflow-hidden">
|
||||
|
||||
@@ -9,9 +9,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'i-tabler-photo-filled',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
},
|
||||
@@ -26,6 +23,7 @@ const props = defineProps({
|
||||
type: Object as PropType<ResultBlockMeta>,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['use-reference'])
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
@@ -37,7 +35,9 @@ const cachedImagesInterval = ref<NodeJS.Timeout | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
cachedImagesInterval.value = setInterval(async () => {
|
||||
cachedImages.value = await get(props.fid) as string[] || []
|
||||
const res = await get(props.fid) as string[] || []
|
||||
if (res.length === cachedImages.value.length) return
|
||||
cachedImages.value = res
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
@@ -54,6 +54,23 @@ const handle_download = (url: string) => {
|
||||
a.click()
|
||||
}
|
||||
|
||||
const handle_use_reference = async (blob_url: string) => {
|
||||
fetch(blob_url)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const file = new File([blob], `xsh_drawing-${props.meta?.datetime! * 1000}.png`, {type: 'image/png'})
|
||||
emit('use-reference', file)
|
||||
})
|
||||
.catch(() => {
|
||||
toast.add({
|
||||
title: '转换失败',
|
||||
description: '无法获取图片数据',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
toast.add({
|
||||
@@ -78,7 +95,7 @@ const copyToClipboard = (text: string) => {
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon :name="icon"/>
|
||||
<h1 class="text-sm font-semibold">
|
||||
{{ title }}
|
||||
{{ meta.type || 'AI 智能绘图' }}
|
||||
</h1>
|
||||
<UDivider class="flex-1" size="sm"/>
|
||||
<UButton color="black" size="xs" icon="i-tabler-info-circle"
|
||||
@@ -103,11 +120,12 @@ const copyToClipboard = (text: string) => {
|
||||
scale-105 opacity-0 group-hover:scale-100 group-hover:opacity-100 transition">
|
||||
<div class="w-full flex justify-end gap-1 p-1">
|
||||
<UTooltip text="以此图为参考创作">
|
||||
<UButton color="indigo" variant="soft" size="2xs" icon="i-tabler-copy" square></UButton>
|
||||
<UButton color="indigo" variant="soft" size="2xs" icon="i-tabler-copy" square
|
||||
@click="handle_use_reference(url)"></UButton>
|
||||
</UTooltip>
|
||||
<UTooltip text="下载">
|
||||
<UButton color="indigo" variant="soft" size="2xs" icon="i-tabler-download" square
|
||||
@click="handle_download(url)"></UButton>
|
||||
@click="handle_download(url)"></UButton>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,6 +142,10 @@ const copyToClipboard = (text: string) => {
|
||||
<UIcon class="text-sm" name="i-tabler-box-seam"/>
|
||||
{{ meta.modal }}
|
||||
</UBadge>
|
||||
<UBadge v-if="meta.style" color="green" variant="subtle" class="text-[10px] font-bold gap-0.5">
|
||||
<UIcon class="text-sm" name="i-tabler-christmas-tree"/>
|
||||
{{ meta.style }}
|
||||
</UBadge>
|
||||
<UBadge v-if="meta.cost" color="amber" variant="subtle" class="text-[10px] font-bold gap-0.5">
|
||||
<UIcon class="text-sm" name="i-solar-fire-bold"/>
|
||||
{{ meta.cost }}
|
||||
|
||||
12
components/aigc/drawing/index.d.ts
vendored
12
components/aigc/drawing/index.d.ts
vendored
@@ -1,7 +1,9 @@
|
||||
export declare interface ResultBlockMeta {
|
||||
modal?: string
|
||||
cost?: string
|
||||
ratio?: string
|
||||
id?: string
|
||||
datetime?: number
|
||||
modal?: string
|
||||
cost?: string
|
||||
ratio?: string
|
||||
id?: string
|
||||
style?: string
|
||||
datetime?: number
|
||||
type?: string
|
||||
}
|
||||
Reference in New Issue
Block a user