feat: 绘画功能本地图片存储优化

ui: 优化字体
This commit is contained in:
2024-03-20 18:00:04 +08:00
parent e69774679a
commit 75f431d7bf
9 changed files with 181 additions and 59 deletions

View File

@@ -1,6 +1,6 @@
export default defineAppConfig({
ui: {
primary: 'green',
primary: 'indigo',
gray: 'neutral',
strategy: 'merge',
button: {

9
assets/css/tailwind.css Normal file
View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Rubik', 'Noto Sans SC', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
}

View File

@@ -2,6 +2,7 @@
import type {ResultBlockMeta} from '~/components/aigc/drawing/index';
import type {PropType} from 'vue';
import dayjs from 'dayjs';
import {get} from 'idb-keyval';
const props = defineProps({
icon: {
@@ -14,17 +15,62 @@ const props = defineProps({
prompt: {
type: String,
},
fid: {
type: String,
required: true,
},
images: {
type: Array,
default: (): Array<string> => [],
},
meta: {
type: Object as PropType<ResultBlockMeta>,
},
})
const toast = useToast()
const expand_prompt = ref(false)
const show_meta = ref(true)
const cachedImages = ref<string[]>([])
const cachedImagesInterval = ref<NodeJS.Timeout | null>(null)
onMounted(async () => {
cachedImagesInterval.value = setInterval(async () => {
cachedImages.value = await get(props.fid) as string[] || []
}, 1000)
})
onUnmounted(() => {
if (cachedImagesInterval.value) {
clearInterval(cachedImagesInterval.value)
}
})
const handle_download = (url: string) => {
const a = document.createElement('a')
a.href = url
a.download = `xsh_ai_drawing-${dayjs(props.meta?.datetime! * 1000).format('YYYY-MM-DD-HH-mm-ss')}.png`
a.click()
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
toast.add({
title: '复制成功',
description: '已将内容复制到剪贴板',
color: 'primary',
icon: 'i-tabler-copy',
})
}).catch(() => {
toast.add({
title: '复制失败',
description: '无法复制到剪贴板',
color: 'red',
icon: 'i-tabler-circle-x',
})
})
}
</script>
<template>
@@ -47,10 +93,27 @@ const show_meta = ref(true)
@click="expand_prompt = !expand_prompt">
{{ prompt }}
</p>
<UButton color="gray" size="xs" icon="i-tabler-copy" variant="ghost" class="-mt-1"></UButton>
<UButton color="gray" size="xs" icon="i-tabler-copy" variant="ghost" class="-mt-1"
@click="copyToClipboard(prompt)"></UButton>
</div>
<div v-if="images.length > 0" class="flex items-center overflow-x-auto h-64 gap-2 pb-2 snap-x">
<img v-for="(url, i) in images" class="result-image" :src="useBlobUrlFromB64(url)" alt="AI Generated" :key="i"/>
<div v-if="cachedImages.length > 0" class="flex items-center overflow-x-auto h-64 gap-2 pb-2 snap-x">
<div class="h-full aspect-auto relative rounded-lg shadow-md overflow-hidden group"
v-for="(url, i) in cachedImages" :key="`${fid}-${i}`">
<div class="absolute inset-0 bg-gradient-to-t from-neutral-800/40 to-transparent w-full h-full flex items-end
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>
</UTooltip>
<UTooltip text="下载">
<UButton color="indigo" variant="soft" size="2xs" icon="i-tabler-download" square
@click="handle_download(url)"></UButton>
</UTooltip>
</div>
</div>
<img class="result-image" :src="useBlobUrlFromB64(url)" alt="AI Generated"/>
</div>
</div>
<div v-else class="h-64 aspect-[3/4] mb-4 rounded-lg placeholder-gradient flex justify-center items-center">
<UIcon name="i-svg-spinners-tadpole" class="text-3xl"/>
@@ -95,7 +158,7 @@ const show_meta = ref(true)
.result-image {
@apply snap-start;
@apply h-full aspect-auto object-cover rounded-lg shadow-md;
@apply w-full h-full object-cover;
}
.placeholder-gradient {

View File

@@ -1,4 +1,3 @@
import {string} from 'yup';
import type {ResultBlockMeta} from '~/components/aigc/drawing';
export interface HistoryItem {
@@ -6,7 +5,7 @@ export interface HistoryItem {
data_id?: string
prompt: string
meta: ResultBlockMeta
images: string[]
images?: string[]
}
export const useHistory = defineStore('aigc_history', () => {

View File

@@ -3,27 +3,34 @@ export default defineNuxtConfig({
devtools: {enabled: true},
runtimeConfig: {
public: {
API_BASE: 'https://service1.fenshenzhike.com/'
}
API_BASE: 'https://service1.fenshenzhike.com/',
},
},
modules: [
'@nuxt/ui',
'radix-vue/nuxt',
'dayjs-nuxt',
"@pinia/nuxt",
"@pinia-plugin-persistedstate/nuxt",
"@vite-pwa/nuxt",
'@pinia/nuxt',
'@pinia-plugin-persistedstate/nuxt',
'@vite-pwa/nuxt',
['@nuxtjs/google-fonts', {
display: 'swap',
families: {
Rubik: '100..900',
'Noto Sans SC': '100..900',
}
}],
],
ui: {
icons: ['tabler', 'solar', 'line-md', 'svg-spinners']
icons: ['tabler', 'solar', 'line-md', 'svg-spinners'],
},
colorMode: {
preference: 'dark'
preference: 'dark',
},
dayjs: {
locales: ['zh', 'en'],
plugins: ['relativeTime', 'utc', 'timezone'],
defaultLocale: 'zh',
defaultTimezone: 'Asia/Shanghai',
}
},
})

View File

@@ -15,6 +15,7 @@
"@iconify-json/svg-spinners": "^1.1.2",
"@iconify-json/tabler": "^1.1.105",
"@nuxt/ui": "^2.14.1",
"idb-keyval": "^6.2.1",
"nuxt": "^3.10.3",
"radix-vue": "^1.4.9",
"vue": "^3.4.19",
@@ -22,6 +23,7 @@
"yup": "^1.4.0"
},
"devDependencies": {
"@nuxtjs/google-fonts": "^3.2.0",
"@pinia-plugin-persistedstate/nuxt": "^1.2.0",
"@pinia/nuxt": "^0.5.1",
"@vite-pwa/nuxt": "^0.5.0",

View File

@@ -9,6 +9,7 @@ import RatioSelector from '~/components/aigc/RatioSelector.vue';
import {useFetchWrapped} from '~/composables/useFetchWrapped';
import type {ResultBlockMeta} from '~/components/aigc/drawing';
import {useHistory} from '~/composables/useHistory';
import {del, set} from 'idb-keyval';
useHead({
title: '绘画 | XSH AI',
@@ -22,6 +23,7 @@ const loginState = useLoginState()
const leftSection = ref<HTMLElement | null>(null)
const leftHandler = ref<HTMLElement | null>(null)
const showSidebar = ref(false)
const generating = ref(false)
@@ -213,7 +215,6 @@ const onDefaultFormSubmit = (event: FormSubmitEvent<DefaultFormSchema>) => {
fid,
meta,
prompt: event.data.prompt,
images: [],
})
const styleItem = event.data.styles as StyleItem
useFetchWrapped<
@@ -233,14 +234,19 @@ const onDefaultFormSubmit = (event: FormSubmitEvent<DefaultFormSchema>) => {
color: 'red',
icon: 'i-tabler-circle-x',
})
history.text2img = history.text2img.filter(item => item.fid !== fid)
return
}
const record = history.text2img.find(item => item.fid === fid)
record!.images = [`data:image/png;base64,${res.data.request_image}`]
record!.meta = {
...record!.meta,
history.text2img = history.text2img.map(item => {
if (item.fid === fid) {
set(`${item.fid}`, [`data:image/png;base64,${res.data.request_image}`])
item.meta = {
...item.meta,
id: res.data.data_id as string,
}
}
return item
})
}).catch(err => {
toast.add({
title: '生成失败',
@@ -255,10 +261,10 @@ const onDefaultFormSubmit = (event: FormSubmitEvent<DefaultFormSchema>) => {
</script>
<template>
<div class="w-full flex">
<div class="w-full flex relative">
<div ref="leftSection"
class="sticky hidden md:block h-[calc(100vh-4rem)] overflow-hidden bg-neutral-200 dark:bg-neutral-800 transition-all"
style="width: 320px">
class="absolute -translate-x-full md:sticky md:translate-x-0 z-10 md:block h-[calc(100vh-4rem)] bg-neutral-200 dark:bg-neutral-800 transition-all"
style="width: 320px" :class="{'translate-x-0': showSidebar}">
<div ref="leftHandler"
class="absolute inset-0 left-auto hidden xl:flex flex-col justify-center items-center cursor-ew-resize px-1 group"
@dblclick="leftSection?.style.setProperty('width', '320px')"
@@ -266,6 +272,10 @@ const onDefaultFormSubmit = (event: FormSubmitEvent<DefaultFormSchema>) => {
<span
class="w-[1px] h-full bg-neutral-300 dark:bg-neutral-700 group-hover:bg-indigo-300 dark:group-hover:bg-indigo-700 group-hover:w-[3px] transition-all group-hover:delay-500 translate-x-1"></span>
</div>
<div
class="absolute bottom-28 -right-12 w-12 h-12 z-10 bg-neutral-100 dark:bg-neutral-800 rounded-r-lg shadow-lg flex md:hidden justify-center items-center">
<UButton color="black" icon="i-tabler-brush" size="lg" square @click="showSidebar = !showSidebar"></UButton>
</div>
<div class="h-full flex flex-col overflow-y-auto">
<UForm :schema="defaultFormSchema" :state="defaultFormState" @submit="onDefaultFormSubmit">
<div class="flex flex-col gap-2 p-4 pb-28">
@@ -324,8 +334,8 @@ const onDefaultFormSubmit = (event: FormSubmitEvent<DefaultFormSchema>) => {
<p class="text-sm text-neutral-500 dark:text-neutral-400">没有记录</p>
</div>
<ResultBlock v-else v-for="(result, k) in history.text2img"
title="文生图" :images="result.images" :meta="result.meta"
:prompt="result.prompt">
title="文生图" :fid="result.fid" :meta="result.meta"
:prompt="result.prompt" :key="result.fid">
<template #header-right>
<UPopover overlay>
<UButton color="black" size="xs" icon="i-tabler-trash" variant="ghost"></UButton>
@@ -337,7 +347,11 @@ const onDefaultFormSubmit = (event: FormSubmitEvent<DefaultFormSchema>) => {
取消
</UButton>
<UButton color="red" size="xs" class="font-bold"
@click="() => {history.text2img.splice(k, 1); close();}">
@click="() => {
history.text2img.splice(k, 1)
del(result.fid)
close()
}">
仍然删除
</UButton>
</div>

View File

@@ -1,13 +1,17 @@
import type {Config} from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>>{
theme: {
fontFamily: {
sans: ['Rubik', 'Noto Sans SC', 'sans-serif'],
},
extend: {
aspectRatio: {
auto: 'auto',
square: '1 / 1',
video: '16 / 9'
}
}
}
video: '16 / 9',
},
},
},
}

View File

@@ -1825,6 +1825,15 @@
lodash.template "^4.5.0"
pathe "^1.1.1"
"@nuxtjs/google-fonts@^3.2.0":
version "3.2.0"
resolved "https://registry.npmmirror.com/@nuxtjs/google-fonts/-/google-fonts-3.2.0.tgz#03e3284dc980e6bcaa0c67c3080aecda76574b1c"
integrity sha512-cGAjDJoeQ2jm6VJCo4AtSmKO6KjsbO9RSLj8q261fD0lMVNMZCxkCxBkg8L0/2Vfgp+5QBHWVXL71p1tiybJFw==
dependencies:
"@nuxt/kit" "^3.10.3"
google-fonts-helper "^3.5.0"
pathe "^1.1.2"
"@nuxtjs/tailwindcss@^6.11.4":
version "6.11.4"
resolved "https://registry.npmmirror.com/@nuxtjs/tailwindcss/-/tailwindcss-6.11.4.tgz#820bc635f47f632637511932f62e4d87184471f8"
@@ -3449,7 +3458,7 @@ deep-equal@~1.0.1:
resolved "https://registry.npmmirror.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==
deepmerge@^4.2.2:
deepmerge@^4.2.2, deepmerge@^4.3.1:
version "4.3.1"
resolved "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
@@ -4262,6 +4271,16 @@ globby@^14.0.0, globby@^14.0.1:
slash "^5.1.0"
unicorn-magic "^0.1.0"
google-fonts-helper@^3.5.0:
version "3.5.0"
resolved "https://registry.npmmirror.com/google-fonts-helper/-/google-fonts-helper-3.5.0.tgz#744f661531b4b7894ce3f6f986ebb1a677df5d89"
integrity sha512-QcKvB3Y+jJFpvBUp/iG1oe9BbKirrjwU2mzJzKGGb5czz6T93CCP9A8IlfCoZnko7b1+gPH3yVghXP5EBvunDg==
dependencies:
deepmerge "^4.3.1"
hookable "^5.5.3"
ofetch "^1.3.3"
ufo "^1.4.0"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@@ -4471,6 +4490,11 @@ iconv-lite@^0.6.2:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^6.2.1:
version "6.2.1"
resolved "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
idb@^7.0.1:
version "7.1.1"
resolved "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"