mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 10:32:25 +00:00
feat:【ele】【ai】image 的迁移初始化
This commit is contained in:
@@ -9,7 +9,8 @@ export namespace AiImageApi {
|
||||
label: string; // Make Variations 文本
|
||||
style: number; // 样式: 2(Primary)、3(Green)
|
||||
}
|
||||
// AI 绘图
|
||||
|
||||
/** AI 绘图 */
|
||||
export interface Image {
|
||||
id: number; // 编号
|
||||
userId: number;
|
||||
@@ -83,6 +84,7 @@ export function deleteImageMy(id: number) {
|
||||
}
|
||||
|
||||
// ================ midjourney 专属 ================
|
||||
|
||||
// 【Midjourney】生成图片
|
||||
export function midjourneyImagine(data: AiImageApi.ImageMidjourneyImagineReq) {
|
||||
return requestClient.post(`/ai/image/midjourney/imagine`, data);
|
||||
@@ -94,6 +96,7 @@ export function midjourneyAction(data: AiImageApi.ImageMidjourneyAction) {
|
||||
}
|
||||
|
||||
// ================ 绘图管理 ================
|
||||
|
||||
// 查询绘画分页
|
||||
export function getImagePage(params: any) {
|
||||
return requestClient.get<AiImageApi.Image[]>(`/ai/image/page`, { params });
|
||||
|
||||
@@ -9,9 +9,11 @@ export namespace AiImageApi {
|
||||
label: string; // Make Variations 文本
|
||||
style: number; // 样式: 2(Primary)、3(Green)
|
||||
}
|
||||
// AI 绘图
|
||||
|
||||
/** AI 绘图 */
|
||||
export interface Image {
|
||||
id: number; // 编号
|
||||
userId: number;
|
||||
platform: string; // 平台
|
||||
model: string; // 模型
|
||||
prompt: string; // 提示词
|
||||
@@ -82,6 +84,7 @@ export function deleteImageMy(id: number) {
|
||||
}
|
||||
|
||||
// ================ midjourney 专属 ================
|
||||
|
||||
// 【Midjourney】生成图片
|
||||
export function midjourneyImagine(data: AiImageApi.ImageMidjourneyImagineReq) {
|
||||
return requestClient.post(`/ai/image/midjourney/imagine`, data);
|
||||
@@ -93,6 +96,7 @@ export function midjourneyAction(data: AiImageApi.ImageMidjourneyAction) {
|
||||
}
|
||||
|
||||
// ================ 绘图管理 ================
|
||||
|
||||
// 查询绘画分页
|
||||
export function getImagePage(params: any) {
|
||||
return requestClient.get<AiImageApi.Image[]>(`/ai/image/page`, { params });
|
||||
|
||||
@@ -9,18 +9,18 @@ const routes: RouteRecordRaw[] = [
|
||||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
// {
|
||||
// path: 'image/square',
|
||||
// component: () => import('#/views/ai/image/square/index.vue'),
|
||||
// name: 'AiImageSquare',
|
||||
// meta: {
|
||||
// noCache: true,
|
||||
// hidden: true,
|
||||
// canTo: true,
|
||||
// title: '绘图作品',
|
||||
// activePath: '/ai/image',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
path: 'image/square',
|
||||
component: () => import('#/views/ai/image/square/index.vue'),
|
||||
name: 'AiImageSquare',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '绘图作品',
|
||||
activePath: '/ai/image',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'knowledge/document',
|
||||
component: () => import('#/views/ai/knowledge/document/index.vue'),
|
||||
|
||||
132
apps/web-ele/src/views/ai/image/index/index.vue
Normal file
132
apps/web-ele/src/views/ai/image/index/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { AiModelTypeEnum, AiPlatformEnum } from '@vben/constants';
|
||||
|
||||
import { ElSegmented } from 'element-plus';
|
||||
|
||||
import { getModelSimpleList } from '#/api/ai/model/model';
|
||||
|
||||
import Common from './modules/common/index.vue';
|
||||
import Dall3 from './modules/dall3/index.vue';
|
||||
import ImageList from './modules/list.vue';
|
||||
import Midjourney from './modules/midjourney/index.vue';
|
||||
import StableDiffusion from './modules/stable-diffusion/index.vue';
|
||||
|
||||
const imageListRef = ref<any>(); // image 列表 ref
|
||||
const dall3Ref = ref<any>(); // dall3(openai) ref
|
||||
const midjourneyRef = ref<any>(); // midjourney ref
|
||||
const stableDiffusionRef = ref<any>(); // stable diffusion ref
|
||||
const commonRef = ref<any>(); // stable diffusion ref
|
||||
|
||||
const selectPlatform = ref('common'); // 选中的平台
|
||||
const platformOptions = [
|
||||
{
|
||||
label: '通用',
|
||||
value: 'common',
|
||||
},
|
||||
{
|
||||
label: 'DALL3 绘画',
|
||||
value: AiPlatformEnum.OPENAI,
|
||||
},
|
||||
{
|
||||
label: 'MJ 绘画',
|
||||
value: AiPlatformEnum.MIDJOURNEY,
|
||||
},
|
||||
{
|
||||
label: 'SD 绘图',
|
||||
value: AiPlatformEnum.STABLE_DIFFUSION,
|
||||
},
|
||||
];
|
||||
const models = ref<AiModelModelApi.Model[]>([]); // 模型列表
|
||||
|
||||
/** 绘画 start */
|
||||
async function handleDrawStart() {}
|
||||
|
||||
/** 绘画 complete */
|
||||
async function handleDrawComplete() {
|
||||
await imageListRef.value.getImageList();
|
||||
}
|
||||
|
||||
/** 重新生成:将画图详情填充到对应平台 */
|
||||
async function handleRegeneration(image: AiImageApi.Image) {
|
||||
// 切换平台
|
||||
selectPlatform.value = image.platform;
|
||||
// 根据不同平台填充 image
|
||||
await nextTick();
|
||||
switch (image.platform) {
|
||||
case AiPlatformEnum.MIDJOURNEY: {
|
||||
midjourneyRef.value.settingValues(image);
|
||||
|
||||
break;
|
||||
}
|
||||
case AiPlatformEnum.OPENAI: {
|
||||
dall3Ref.value.settingValues(image);
|
||||
|
||||
break;
|
||||
}
|
||||
case AiPlatformEnum.STABLE_DIFFUSION: {
|
||||
stableDiffusionRef.value.settingValues(image);
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
// TODO @fan:貌似 other 重新设置不行?
|
||||
}
|
||||
|
||||
/** 组件挂载的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取模型列表
|
||||
models.value = await getModelSimpleList(AiModelTypeEnum.IMAGE);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="absolute inset-0 m-4 flex h-full w-full flex-row">
|
||||
<div class="bg-card left-0 mr-4 flex w-96 flex-col rounded-lg p-4">
|
||||
<div class="flex justify-center">
|
||||
<ElSegmented
|
||||
v-model="selectPlatform"
|
||||
:options="platformOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-8 h-full overflow-y-auto">
|
||||
<Common
|
||||
v-if="selectPlatform === 'common'"
|
||||
ref="commonRef"
|
||||
:models="models"
|
||||
@on-draw-complete="handleDrawComplete"
|
||||
/>
|
||||
<Dall3
|
||||
v-if="selectPlatform === AiPlatformEnum.OPENAI"
|
||||
ref="dall3Ref"
|
||||
:models="models"
|
||||
@on-draw-start="handleDrawStart"
|
||||
@on-draw-complete="handleDrawComplete"
|
||||
/>
|
||||
<Midjourney
|
||||
v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
|
||||
ref="midjourneyRef"
|
||||
:models="models"
|
||||
/>
|
||||
<StableDiffusion
|
||||
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
|
||||
ref="stableDiffusionRef"
|
||||
:models="models"
|
||||
@on-draw-complete="handleDrawComplete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-card flex-1">
|
||||
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
135
apps/web-ele/src/views/ai/image/index/modules/card.vue
Normal file
135
apps/web-ele/src/views/ai/image/index/modules/card.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
|
||||
import { onMounted, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { AiImageStatusEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElButton, ElCard, ElImage, ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
detail: {
|
||||
type: Object as PropType<AiImageApi.Image>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']);
|
||||
|
||||
const cardImageRef = ref<any>(); // 卡片 image ref
|
||||
|
||||
/** 处理点击事件 */
|
||||
async function handleButtonClick(type: string, detail: AiImageApi.Image) {
|
||||
emits('onBtnClick', type, detail);
|
||||
}
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
async function handleMidjourneyBtnClick(
|
||||
button: AiImageApi.ImageMidjourneyButtons,
|
||||
) {
|
||||
await confirm(`确认操作 "${button.label} ${button.emoji}" ?`);
|
||||
emits('onMjBtnClick', button, props.detail);
|
||||
}
|
||||
|
||||
/** 监听详情 */
|
||||
const { detail } = toRefs(props);
|
||||
watch(detail, async (newVal) => {
|
||||
await handleLoading(newVal.status);
|
||||
});
|
||||
const loading = ref();
|
||||
|
||||
/** 处理加载状态 */
|
||||
async function handleLoading(status: number) {
|
||||
// 情况一:如果是生成中,则设置加载中的 loading
|
||||
if (status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
loading.value = ElMessage({
|
||||
message: `生成中...`,
|
||||
type: 'info',
|
||||
duration: 0,
|
||||
});
|
||||
} else {
|
||||
// 情况二:如果已经生成结束,则移除 loading
|
||||
if (loading.value) {
|
||||
setTimeout(() => loading.value.close(), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await handleLoading(props.detail.status);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ElCard class="relative flex h-auto w-80 flex-col rounded-lg">
|
||||
<!-- 图片操作区 -->
|
||||
<div class="flex flex-row justify-between">
|
||||
<div>
|
||||
<ElButton v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
|
||||
生成中
|
||||
</ElButton>
|
||||
<ElButton v-else-if="detail?.status === AiImageStatusEnum.SUCCESS">
|
||||
已完成
|
||||
</ElButton>
|
||||
<ElButton type="danger" v-else-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||
异常
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<ElButton
|
||||
class="m-0 p-2"
|
||||
text
|
||||
@click="handleButtonClick('download', detail)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:download" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="m-0 p-2"
|
||||
text
|
||||
@click="handleButtonClick('regeneration', detail)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:refresh-cw" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="m-0 p-2"
|
||||
text
|
||||
@click="handleButtonClick('delete', detail)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="m-0 p-2"
|
||||
text
|
||||
@click="handleButtonClick('more', detail)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片展示区域 -->
|
||||
<div class="mt-5 h-72 flex-1 overflow-hidden" ref="cardImageRef">
|
||||
<ElImage class="w-full rounded-lg" :src="detail?.picUrl" />
|
||||
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||
{{ detail?.errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Midjourney 专属操作按钮 -->
|
||||
<div class="mt-2 flex w-full flex-wrap justify-start">
|
||||
<ElButton
|
||||
size="small"
|
||||
v-for="(button, index) in detail?.buttons"
|
||||
:key="index"
|
||||
class="m-2 ml-0 min-w-10"
|
||||
@click="handleMidjourneyBtnClick(button)"
|
||||
>
|
||||
{{ button.label }}{{ button.emoji }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
233
apps/web-ele/src/views/ai/image/index/modules/common/index.vue
Normal file
233
apps/web-ele/src/views/ai/image/index/modules/common/index.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<!-- dall3 -->
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
ImageHotWords,
|
||||
OtherPlatformEnum,
|
||||
} from '@vben/constants';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { drawImage } from '#/api/ai/image';
|
||||
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
}); // 接收父组件传入的模型列表
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
|
||||
const drawIn = ref<boolean>(false); // 生成中
|
||||
const selectHotWord = ref<string>(''); // 选中的热词
|
||||
|
||||
const prompt = ref<string>(''); // 提示词
|
||||
const width = ref<number>(512); // 图片宽度
|
||||
const height = ref<number>(512); // 图片高度
|
||||
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI); // 平台
|
||||
const platformModels = ref<AiModelModelApi.Model[]>([]); // 模型列表
|
||||
const modelId = ref<number>(); // 选中的模型
|
||||
|
||||
/** 选择热词 */
|
||||
async function handleHotWordClick(hotWord: string) {
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value === hotWord) {
|
||||
selectHotWord.value = '';
|
||||
return;
|
||||
}
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord; // 选中
|
||||
prompt.value = hotWord; // 替换提示词
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
async function handleGenerateImage() {
|
||||
// 二次确认
|
||||
await confirm(`确认生成内容?`);
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true;
|
||||
// 回调
|
||||
emits('onDrawStart', otherPlatform.value);
|
||||
// 发送请求
|
||||
const form = {
|
||||
platform: otherPlatform.value,
|
||||
modelId: modelId.value, // 模型
|
||||
prompt: prompt.value, // 提示词
|
||||
width: width.value, // 图片宽度
|
||||
height: height.value, // 图片高度
|
||||
options: {},
|
||||
} as unknown as AiImageApi.ImageDrawReq;
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', otherPlatform.value);
|
||||
// 加载结束
|
||||
drawIn.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
prompt.value = detail.prompt;
|
||||
width.value = detail.width;
|
||||
height.value = detail.height;
|
||||
}
|
||||
|
||||
/** 平台切换 */
|
||||
async function handlerPlatformChange(platform: any) {
|
||||
// 根据选择的平台筛选模型
|
||||
platformModels.value = props.models.filter(
|
||||
(item: AiModelModelApi.Model) => item.platform === platform,
|
||||
);
|
||||
// 切换平台,默认选择一个模型
|
||||
modelId.value =
|
||||
platformModels.value.length > 0 && platformModels.value[0]
|
||||
? platformModels.value[0].id
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** 监听 models 变化 */
|
||||
watch(
|
||||
() => props.models,
|
||||
() => {
|
||||
handlerPlatformChange(otherPlatform.value);
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues });
|
||||
</script>
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<b>画面描述</b>
|
||||
<p>建议使用"形容词 + 动词 + 风格"的格式,使用","隔开</p>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:maxlength="1024"
|
||||
:rows="5"
|
||||
type="textarea"
|
||||
class="mt-4 w-full"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col">
|
||||
<div>
|
||||
<b>随机热词</b>
|
||||
</div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap justify-start">
|
||||
<ElButton
|
||||
round
|
||||
class="m-0"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div>
|
||||
<b>平台</b>
|
||||
</div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElSelect
|
||||
v-model="otherPlatform"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
@change="handlerPlatformChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in OtherPlatformEnum"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div>
|
||||
<b>模型</b>
|
||||
</div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElSelect
|
||||
v-model="modelId"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in platformModels"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div>
|
||||
<b>图片尺寸</b>
|
||||
</div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap gap-x-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>宽</span>
|
||||
<ElInputNumber
|
||||
v-model="width"
|
||||
placeholder="图片宽度"
|
||||
controls-position="right"
|
||||
class="!w-32"
|
||||
/>
|
||||
<span>px</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>高</span>
|
||||
<ElInputNumber
|
||||
v-model="height"
|
||||
placeholder="图片高度"
|
||||
controls-position="right"
|
||||
class="!w-32"
|
||||
/>
|
||||
<span>px</span>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
round
|
||||
:loading="drawIn"
|
||||
:disabled="prompt.length === 0"
|
||||
@click="handleGenerateImage"
|
||||
>
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
259
apps/web-ele/src/views/ai/image/index/modules/dall3/index.vue
Normal file
259
apps/web-ele/src/views/ai/image/index/modules/dall3/index.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<!-- dall3 -->
|
||||
<script setup lang="ts">
|
||||
import type { ImageModel, ImageSize } from '@vben/constants';
|
||||
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
Dall3Models,
|
||||
Dall3SizeList,
|
||||
Dall3StyleList,
|
||||
ImageHotWords,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { ElButton, ElImage, ElMessage, ElSpace } from 'element-plus';
|
||||
|
||||
import { drawImage } from '#/api/ai/image';
|
||||
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
}); // 接收父组件传入的模型列表
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
|
||||
const prompt = ref<string>(''); // 提示词
|
||||
const drawIn = ref<boolean>(false); // 生成中
|
||||
const selectHotWord = ref<string>(''); // 选中的热词
|
||||
const selectModel = ref<string>('dall-e-3'); // 模型
|
||||
const selectSize = ref<string>('1024x1024'); // 选中 size
|
||||
const style = ref<string>('vivid'); // style 样式
|
||||
|
||||
/** 选择热词 */
|
||||
async function handleHotWordClick(hotWord: string) {
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value === hotWord) {
|
||||
selectHotWord.value = '';
|
||||
return;
|
||||
}
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord;
|
||||
prompt.value = hotWord;
|
||||
}
|
||||
|
||||
/** 选择 model 模型 */
|
||||
async function handleModelClick(model: ImageModel) {
|
||||
selectModel.value = model.key;
|
||||
// 可以在这里添加模型特定的处理逻辑
|
||||
// 例如,如果未来需要根据不同模型设置不同参数
|
||||
if (model.key === 'dall-e-3') {
|
||||
// DALL-E-3 模型特定的处理
|
||||
style.value = 'vivid'; // 默认设置vivid风格
|
||||
} else if (model.key === 'dall-e-2') {
|
||||
// DALL-E-2 模型特定的处理
|
||||
style.value = 'natural'; // 如果有其他DALL-E-2适合的默认风格
|
||||
}
|
||||
|
||||
// 更新其他相关参数
|
||||
// 例如可以默认选择最适合当前模型的尺寸
|
||||
const recommendedSize = Dall3SizeList.find(
|
||||
(size) =>
|
||||
(model.key === 'dall-e-3' && size.key === '1024x1024') ||
|
||||
(model.key === 'dall-e-2' && size.key === '512x512'),
|
||||
);
|
||||
|
||||
if (recommendedSize) {
|
||||
selectSize.value = recommendedSize.key;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择 style 样式 */
|
||||
async function handleStyleClick(imageStyle: ImageModel) {
|
||||
style.value = imageStyle.key;
|
||||
}
|
||||
|
||||
/** 选择 size 大小 */
|
||||
async function handleSizeClick(imageSize: ImageSize) {
|
||||
selectSize.value = imageSize.key;
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
async function handleGenerateImage() {
|
||||
// 从 models 中查找匹配的模型
|
||||
const matchedModel = props.models.find(
|
||||
(item) =>
|
||||
item.model === selectModel.value &&
|
||||
item.platform === AiPlatformEnum.OPENAI,
|
||||
);
|
||||
if (!matchedModel) {
|
||||
ElMessage.error('该模型不可用,请选择其它模型');
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
await confirm(`确认生成内容?`);
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true;
|
||||
// 回调
|
||||
emits('onDrawStart', AiPlatformEnum.OPENAI);
|
||||
const imageSize = Dall3SizeList.find(
|
||||
(item) => item.key === selectSize.value,
|
||||
) as ImageSize;
|
||||
const form = {
|
||||
platform: AiPlatformEnum.OPENAI,
|
||||
prompt: prompt.value, // 提示词
|
||||
modelId: matchedModel.id, // 使用匹配到的模型
|
||||
style: style.value, // 图像生成的风格
|
||||
width: imageSize.width, // size 不能为空
|
||||
height: imageSize.height, // size 不能为空
|
||||
options: {
|
||||
style: style.value, // 图像生成的风格
|
||||
},
|
||||
} as AiImageApi.ImageDrawReq;
|
||||
// 发送请求
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', AiPlatformEnum.OPENAI);
|
||||
// 加载结束
|
||||
drawIn.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
prompt.value = detail.prompt;
|
||||
selectModel.value = detail.model;
|
||||
style.value = detail.options?.style;
|
||||
const imageSize = Dall3SizeList.find(
|
||||
(item) => item.key === `${detail.width}x${detail.height}`,
|
||||
) as ImageSize;
|
||||
await handleSizeClick(imageSize);
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues });
|
||||
</script>
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<b>画面描述</b>
|
||||
<p>建议使用"形容词 + 动词 + 风格"的格式,使用","隔开</p>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:maxlength="1024"
|
||||
:rows="5"
|
||||
type="textarea"
|
||||
class="mt-4 w-full"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col">
|
||||
<div><b>随机热词</b></div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap justify-start">
|
||||
<ElButton
|
||||
round
|
||||
class="m-0"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div><b>模型选择</b></div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap gap-2">
|
||||
<div
|
||||
class="flex w-28 cursor-pointer flex-col items-center overflow-hidden rounded-lg border-2"
|
||||
:class="[
|
||||
selectModel === model.key ? '!border-blue-500' : 'border-transparent',
|
||||
]"
|
||||
v-for="model in Dall3Models"
|
||||
:key="model.key"
|
||||
>
|
||||
<ElImage
|
||||
:preview-src-list="[]"
|
||||
:src="model.image"
|
||||
fit="contain"
|
||||
@click="handleModelClick(model)"
|
||||
/>
|
||||
<div class="text-sm font-bold text-gray-600">
|
||||
{{ model.name }}
|
||||
</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div><b>风格选择</b></div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap gap-2">
|
||||
<div
|
||||
class="flex w-28 cursor-pointer flex-col items-center overflow-hidden rounded-lg border-2"
|
||||
:class="[
|
||||
style === imageStyle.key ? 'border-blue-500' : 'border-transparent',
|
||||
]"
|
||||
v-for="imageStyle in Dall3StyleList"
|
||||
:key="imageStyle.key"
|
||||
>
|
||||
<ElImage
|
||||
:preview-src-list="[]"
|
||||
:src="imageStyle.image"
|
||||
fit="contain"
|
||||
@click="handleStyleClick(imageStyle)"
|
||||
/>
|
||||
<div class="text-sm font-bold text-gray-600">
|
||||
{{ imageStyle.name }}
|
||||
</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 w-full">
|
||||
<div><b>画面比例</b></div>
|
||||
<ElSpace wrap class="mt-5 flex w-full flex-wrap gap-2">
|
||||
<div
|
||||
class="flex cursor-pointer flex-col items-center"
|
||||
v-for="imageSize in Dall3SizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handleSizeClick(imageSize)"
|
||||
>
|
||||
<div
|
||||
class="bg-card flex h-12 w-12 flex-col items-center justify-center rounded-lg border p-0"
|
||||
:class="[
|
||||
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
||||
]"
|
||||
>
|
||||
<div :style="imageSize.style"></div>
|
||||
</div>
|
||||
<div class="text-sm font-bold text-gray-600">
|
||||
{{ imageSize.name }}
|
||||
</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
round
|
||||
:loading="drawIn"
|
||||
:disabled="prompt.length === 0"
|
||||
@click="handleGenerateImage"
|
||||
>
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
210
apps/web-ele/src/views/ai/image/index/modules/detail.vue
Normal file
210
apps/web-ele/src/views/ai/image/index/modules/detail.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
Dall3StyleList,
|
||||
StableDiffusionClipGuidancePresets,
|
||||
StableDiffusionSamplers,
|
||||
StableDiffusionStylePresets,
|
||||
} from '@vben/constants';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
import { getImageMy } from '#/api/ai/image';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const detail = ref<AiImageApi.Image>({} as AiImageApi.Image); // 图片详细信息
|
||||
|
||||
/** 获取图片详情 */
|
||||
async function getImageDetail(id: number) {
|
||||
detail.value = await getImageMy(id);
|
||||
}
|
||||
|
||||
const { id } = toRefs(props);
|
||||
watch(
|
||||
id,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await getImageDetail(newVal);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 w-full overflow-hidden break-words">
|
||||
<div class="mt-2">
|
||||
<ElImage class="rounded-lg" :src="detail?.picUrl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间 -->
|
||||
<div class="mb-5 w-full overflow-hidden break-words">
|
||||
<div class="text-lg font-bold">时间</div>
|
||||
<div class="mt-2">
|
||||
<div>
|
||||
提交时间:{{ formatDate(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}
|
||||
</div>
|
||||
<div>
|
||||
生成时间:{{ formatDate(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型 -->
|
||||
<div class="mb-5 w-full overflow-hidden break-words">
|
||||
<div class="text-lg font-bold">模型</div>
|
||||
<div class="mt-2">
|
||||
{{ detail.model }}({{ detail.height }}x{{ detail.width }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词 -->
|
||||
<div class="mb-5 w-full overflow-hidden break-words">
|
||||
<div class="text-lg font-bold">提示词</div>
|
||||
<div class="mt-2">
|
||||
{{ detail.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片地址 -->
|
||||
<div class="mb-5 w-full overflow-hidden break-words">
|
||||
<div class="text-lg font-bold">图片地址</div>
|
||||
<div class="mt-2">
|
||||
{{ detail.picUrl }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- StableDiffusion 专属 -->
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||
detail?.options?.sampler
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">采样方法</div>
|
||||
<div class="mt-2">
|
||||
{{
|
||||
StableDiffusionSamplers.find(
|
||||
(item) => item.key === detail?.options?.sampler,
|
||||
)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||
detail?.options?.clipGuidancePreset
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">CLIP</div>
|
||||
<div class="mt-2">
|
||||
{{
|
||||
StableDiffusionClipGuidancePresets.find(
|
||||
(item) => item.key === detail?.options?.clipGuidancePreset,
|
||||
)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||
detail?.options?.stylePreset
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">风格</div>
|
||||
<div class="mt-2">
|
||||
{{
|
||||
StableDiffusionStylePresets.find(
|
||||
(item) => item.key === detail?.options?.stylePreset,
|
||||
)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||
detail?.options?.steps
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">迭代步数</div>
|
||||
<div class="mt-2">{{ detail?.options?.steps }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||
detail?.options?.scale
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">引导系数</div>
|
||||
<div class="mt-2">{{ detail?.options?.scale }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||
detail?.options?.seed
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">随机因子</div>
|
||||
<div class="mt-2">{{ detail?.options?.seed }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Dall3 专属 -->
|
||||
<div
|
||||
v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">风格选择</div>
|
||||
<div class="mt-2">
|
||||
{{
|
||||
Dall3StyleList.find((item) => item.key === detail?.options?.style)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Midjourney 专属 -->
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">模型版本</div>
|
||||
<div class="mt-2">{{ detail?.options?.version }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.MIDJOURNEY &&
|
||||
detail?.options?.referImageUrl
|
||||
"
|
||||
class="mb-5 w-full overflow-hidden break-words"
|
||||
>
|
||||
<div class="text-lg font-bold">参考图</div>
|
||||
<div class="mt-2">
|
||||
<ElImage :src="detail.options.referImageUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
222
apps/web-ele/src/views/ai/image/index/modules/list.vue
Normal file
222
apps/web-ele/src/views/ai/image/index/modules/list.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, useVbenDrawer } from '@vben/common-ui';
|
||||
import { AiImageStatusEnum } from '@vben/constants';
|
||||
import { downloadFileFromImageUrl } from '@vben/utils';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { ElButton, ElCard, ElMessage, ElPagination } from 'element-plus';
|
||||
|
||||
import {
|
||||
deleteImageMy,
|
||||
getImageListMyByIds,
|
||||
getImagePageMy,
|
||||
midjourneyAction,
|
||||
} from '#/api/ai/image';
|
||||
|
||||
import ImageCard from './card.vue';
|
||||
import ImageDetail from './detail.vue';
|
||||
|
||||
const emits = defineEmits(['onRegeneration']);
|
||||
const router = useRouter();
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: '图片详情',
|
||||
footer: false,
|
||||
});
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
}); // 图片分页相关的参数
|
||||
const pageTotal = ref<number>(0); // page size
|
||||
const imageList = ref<AiImageApi.Image[]>([]); // image 列表
|
||||
const imageListRef = ref<any>(); // ref
|
||||
|
||||
const inProgressImageMap = ref<{}>({}); // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||
const inProgressTimer = ref<any>(); // 生成中的 image 定时器,轮询生成进展
|
||||
const showImageDetailId = ref<number>(0); // 图片详情的图片编号
|
||||
|
||||
/** 处理查看绘图作品 */
|
||||
function handleViewPublic() {
|
||||
router.push({
|
||||
name: 'AiImageSquare',
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看图片的详情 */
|
||||
async function handleDetailOpen() {
|
||||
drawerApi.open();
|
||||
}
|
||||
/** 获得 image 图片列表 */
|
||||
async function getImageList() {
|
||||
const loading = ElMessage({
|
||||
message: `加载中...`,
|
||||
type: 'info',
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
// 1. 加载图片列表
|
||||
const { list, total } = await getImagePageMy(queryParams);
|
||||
imageList.value = list;
|
||||
pageTotal.value = total;
|
||||
|
||||
// 2. 计算需要轮询的图片
|
||||
const newWatImages: any = {};
|
||||
imageList.value.forEach((item: any) => {
|
||||
if (item.status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
newWatImages[item.id] = item;
|
||||
}
|
||||
});
|
||||
inProgressImageMap.value = newWatImages;
|
||||
} finally {
|
||||
// 关闭正在"加载中"的 Loading
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
|
||||
const debounceGetImageList = useDebounceFn(getImageList, 80);
|
||||
/** 轮询生成中的 image 列表 */
|
||||
async function refreshWatchImages() {
|
||||
const imageIds = Object.keys(inProgressImageMap.value).map(Number);
|
||||
if (imageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const list = (await getImageListMyByIds(imageIds)) as AiImageApi.Image[];
|
||||
const newWatchImages: any = {};
|
||||
list.forEach((image) => {
|
||||
if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
newWatchImages[image.id] = image;
|
||||
} else {
|
||||
const index = imageList.value.findIndex(
|
||||
(oldImage) => image.id === oldImage.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
// 更新 imageList
|
||||
imageList.value[index] = image;
|
||||
}
|
||||
}
|
||||
});
|
||||
inProgressImageMap.value = newWatchImages;
|
||||
}
|
||||
|
||||
/** 图片的点击事件 */
|
||||
async function handleImageButtonClick(
|
||||
type: string,
|
||||
imageDetail: AiImageApi.Image,
|
||||
) {
|
||||
// 详情
|
||||
if (type === 'more') {
|
||||
showImageDetailId.value = imageDetail.id;
|
||||
await handleDetailOpen();
|
||||
return;
|
||||
}
|
||||
// 删除
|
||||
if (type === 'delete') {
|
||||
await confirm(`是否删除照片?`);
|
||||
await deleteImageMy(imageDetail.id);
|
||||
await getImageList();
|
||||
ElMessage.success('删除成功!');
|
||||
return;
|
||||
}
|
||||
// 下载
|
||||
if (type === 'download') {
|
||||
await downloadFileFromImageUrl({
|
||||
fileName: imageDetail.model,
|
||||
source: imageDetail.picUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 重新生成
|
||||
if (type === 'regeneration') {
|
||||
emits('onRegeneration', imageDetail);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
async function handleImageMidjourneyButtonClick(
|
||||
button: AiImageApi.ImageMidjourneyButtons,
|
||||
imageDetail: AiImageApi.Image,
|
||||
) {
|
||||
// 1. 构建 params 参数
|
||||
const data = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId,
|
||||
} as AiImageApi.ImageMidjourneyAction;
|
||||
// 2. 发送 action
|
||||
await midjourneyAction(data);
|
||||
// 3. 刷新列表
|
||||
await getImageList();
|
||||
}
|
||||
|
||||
defineExpose({ getImageList });
|
||||
|
||||
/** 组件挂在的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取 image 列表
|
||||
await getImageList();
|
||||
// 自动刷新 image 列表
|
||||
inProgressTimer.value = setInterval(async () => {
|
||||
await refreshWatchImages();
|
||||
}, 1000 * 3);
|
||||
});
|
||||
|
||||
/** 组件取消挂在的时候 */
|
||||
onUnmounted(async () => {
|
||||
if (inProgressTimer.value) {
|
||||
clearInterval(inProgressTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer class="w-2/5">
|
||||
<ImageDetail :id="showImageDetailId" />
|
||||
</Drawer>
|
||||
<ElCard
|
||||
class="flex h-full w-full flex-col"
|
||||
:body-style="{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
绘画任务
|
||||
<ElButton @click="handleViewPublic">绘画作品</ElButton>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="flex flex-1 flex-wrap content-start overflow-y-auto p-3 pb-28 pt-5"
|
||||
ref="imageListRef"
|
||||
>
|
||||
<ImageCard
|
||||
v-for="image in imageList"
|
||||
:key="image.id"
|
||||
:detail="image"
|
||||
@on-btn-click="handleImageButtonClick"
|
||||
@on-mj-btn-click="handleImageMidjourneyButtonClick"
|
||||
class="mb-3 mr-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-card sticky bottom-0 z-50 flex h-16 items-center justify-center shadow-sm"
|
||||
>
|
||||
<ElPagination
|
||||
:total="pageTotal"
|
||||
v-model:current-page="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 30, 40, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="debounceGetImageList"
|
||||
@current-change="debounceGetImageList"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
@@ -0,0 +1,257 @@
|
||||
<!-- dall3 -->
|
||||
<script setup lang="ts">
|
||||
import type { ImageModel, ImageSize } from '@vben/constants';
|
||||
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
ImageHotWords,
|
||||
MidjourneyModels,
|
||||
MidjourneySizeList,
|
||||
MidjourneyVersions,
|
||||
NijiVersionList,
|
||||
} from '@vben/constants';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElImage,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { midjourneyImagine } from '#/api/ai/image';
|
||||
import { ImageUpload } from '#/components/upload';
|
||||
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
}); // 接收父组件传入的模型列表
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
|
||||
const drawIn = ref<boolean>(false); // 生成中
|
||||
const selectHotWord = ref<string>(''); // 选中的热词
|
||||
|
||||
const prompt = ref<string>(''); // 提示词
|
||||
const referImageUrl = ref<any>(); // 参考图
|
||||
const selectModel = ref<string>('midjourney'); // 选中的模型
|
||||
const selectSize = ref<string>('1:1'); // 选中 size
|
||||
const selectVersion = ref<any>('6.0'); // 选中的 version
|
||||
const versionList = ref<any>(MidjourneyVersions); // version 列表
|
||||
|
||||
/** 选择热词 */
|
||||
async function handleHotWordClick(hotWord: string) {
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value === hotWord) {
|
||||
selectHotWord.value = '';
|
||||
return;
|
||||
}
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord; // 选中
|
||||
prompt.value = hotWord; // 设置提示次
|
||||
}
|
||||
|
||||
/** 点击 size 尺寸 */
|
||||
async function handleSizeClick(imageSize: ImageSize) {
|
||||
selectSize.value = imageSize.key;
|
||||
}
|
||||
|
||||
/** 点击 model 模型 */
|
||||
async function handleModelClick(model: ImageModel) {
|
||||
selectModel.value = model.key;
|
||||
versionList.value =
|
||||
model.key === 'niji' ? NijiVersionList : MidjourneyVersions;
|
||||
selectVersion.value = versionList.value[0].value;
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
async function handleGenerateImage() {
|
||||
// 从 models 中查找匹配的模型
|
||||
const matchedModel = props.models.find(
|
||||
(item) =>
|
||||
item.model === selectModel.value &&
|
||||
item.platform === AiPlatformEnum.MIDJOURNEY,
|
||||
);
|
||||
if (!matchedModel) {
|
||||
ElMessage.error('该模型不可用,请选择其它模型');
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
await confirm(`确认生成内容?`);
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true;
|
||||
// 回调
|
||||
emits('onDrawStart', AiPlatformEnum.MIDJOURNEY);
|
||||
// 发送请求
|
||||
const imageSize = MidjourneySizeList.find(
|
||||
(item) => selectSize.value === item.key,
|
||||
) as ImageSize;
|
||||
const req = {
|
||||
prompt: prompt.value,
|
||||
modelId: matchedModel.id,
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
version: selectVersion.value,
|
||||
referImageUrl: referImageUrl.value,
|
||||
} as AiImageApi.ImageMidjourneyImagineReq;
|
||||
await midjourneyImagine(req);
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', AiPlatformEnum.MIDJOURNEY);
|
||||
// 加载结束
|
||||
drawIn.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
// 提示词
|
||||
prompt.value = detail.prompt;
|
||||
// image size
|
||||
const imageSize = MidjourneySizeList.find(
|
||||
(item) => item.key === `${detail.width}:${detail.height}`,
|
||||
) as ImageSize;
|
||||
selectSize.value = imageSize.key;
|
||||
// 选中模型
|
||||
const model = MidjourneyModels.find(
|
||||
(item) => item.key === detail.options?.model,
|
||||
) as ImageModel;
|
||||
await handleModelClick(model);
|
||||
// 版本
|
||||
selectVersion.value = versionList.value.find(
|
||||
(item: any) => item.value === detail.options?.version,
|
||||
).value;
|
||||
// image
|
||||
referImageUrl.value = detail.options.referImageUrl;
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues });
|
||||
</script>
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<b>画面描述</b>
|
||||
<p>建议使用"形容词+动词+风格"的格式,使用","隔开.</p>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:maxlength="1024"
|
||||
:rows="5"
|
||||
type="textarea"
|
||||
class="mt-4 w-full"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col">
|
||||
<div><b>随机热词</b></div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap gap-2">
|
||||
<ElButton
|
||||
round
|
||||
class="m-0"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div><b>尺寸</b></div>
|
||||
<ElSpace wrap class="mt-4 flex w-full flex-wrap gap-2">
|
||||
<div
|
||||
class="flex cursor-pointer flex-col items-center overflow-hidden"
|
||||
v-for="imageSize in MidjourneySizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handleSizeClick(imageSize)"
|
||||
>
|
||||
<div
|
||||
class="bg-card flex h-12 w-12 items-center justify-center rounded-lg border p-0"
|
||||
:class="[
|
||||
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
||||
]"
|
||||
>
|
||||
<div :style="imageSize.style"></div>
|
||||
</div>
|
||||
<div class="text-sm font-bold text-gray-600">{{ imageSize.key }}</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div><b>模型</b></div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="model in MidjourneyModels"
|
||||
:key="model.key"
|
||||
class="flex max-w-40 cursor-pointer flex-col items-center overflow-hidden"
|
||||
:class="[
|
||||
selectModel === model.key
|
||||
? 'rounded border-blue-500'
|
||||
: 'border-transparent',
|
||||
]"
|
||||
>
|
||||
<ElImage
|
||||
:preview-src-list="[]"
|
||||
:src="model.image"
|
||||
fit="contain"
|
||||
@click="handleModelClick(model)"
|
||||
/>
|
||||
<div class="text-sm font-bold text-gray-600">{{ model.name }}</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div><b>版本</b></div>
|
||||
<ElSpace wrap class="mt-5 flex w-full flex-wrap gap-2">
|
||||
<ElSelect
|
||||
v-model="selectVersion"
|
||||
class="!w-80"
|
||||
clearable
|
||||
placeholder="请选择版本"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in versionList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div><b>参考图</b></div>
|
||||
<ElSpace wrap class="mt-4">
|
||||
<ImageUpload v-model:value="referImageUrl" :show-description="false" />
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center">
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
round
|
||||
:disabled="prompt.length === 0"
|
||||
@click="handleGenerateImage"
|
||||
>
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,312 @@
|
||||
<!-- dall3 -->
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { alert, confirm } from '@vben/common-ui';
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
ImageHotEnglishWords,
|
||||
StableDiffusionClipGuidancePresets,
|
||||
StableDiffusionSamplers,
|
||||
StableDiffusionStylePresets,
|
||||
} from '@vben/constants';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { drawImage } from '#/api/ai/image';
|
||||
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
}); // 接收父组件传入的模型列表
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
|
||||
function hasChinese(str: string) {
|
||||
return /[\u4E00-\u9FA5]/.test(str);
|
||||
}
|
||||
|
||||
// 定义属性
|
||||
const drawIn = ref<boolean>(false); // 生成中
|
||||
const selectHotWord = ref<string>(''); // 选中的热词
|
||||
// 表单
|
||||
const prompt = ref<string>(''); // 提示词
|
||||
const width = ref<number>(512); // 图片宽度
|
||||
const height = ref<number>(512); // 图片高度
|
||||
const sampler = ref<string>('DDIM'); // 采样方法
|
||||
const steps = ref<number>(20); // 迭代步数
|
||||
const seed = ref<number>(42); // 控制生成图像的随机性
|
||||
const scale = ref<number>(7.5); // 引导系数
|
||||
const clipGuidancePreset = ref<string>('NONE'); // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
|
||||
const stylePreset = ref<string>('3d-model'); // 风格
|
||||
|
||||
/** 选择热词 */
|
||||
async function handleHotWordClick(hotWord: string) {
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value === hotWord) {
|
||||
selectHotWord.value = '';
|
||||
return;
|
||||
}
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord; // 选中
|
||||
prompt.value = hotWord; // 替换提示词
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
async function handleGenerateImage() {
|
||||
// 从 models 中查找匹配的模型
|
||||
const selectModel = 'stable-diffusion-v1-6';
|
||||
const matchedModel = props.models.find(
|
||||
(item) =>
|
||||
item.model === selectModel &&
|
||||
item.platform === AiPlatformEnum.STABLE_DIFFUSION,
|
||||
);
|
||||
if (!matchedModel) {
|
||||
ElMessage.error('该模型不可用,请选择其它模型');
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
if (hasChinese(prompt.value)) {
|
||||
await alert('暂不支持中文!');
|
||||
return;
|
||||
}
|
||||
await confirm(`确认生成内容?`);
|
||||
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true;
|
||||
// 回调
|
||||
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION);
|
||||
// 发送请求
|
||||
const form = {
|
||||
modelId: matchedModel.id,
|
||||
prompt: prompt.value, // 提示词
|
||||
width: width.value, // 图片宽度
|
||||
height: height.value, // 图片高度
|
||||
options: {
|
||||
seed: seed.value, // 随机种子
|
||||
steps: steps.value, // 图片生成步数
|
||||
scale: scale.value, // 引导系数
|
||||
sampler: sampler.value, // 采样算法
|
||||
clipGuidancePreset: clipGuidancePreset.value, // 文本提示相匹配的图像 CLIP
|
||||
stylePreset: stylePreset.value, // 风格
|
||||
},
|
||||
} as unknown as AiImageApi.ImageDrawReq;
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION);
|
||||
// 加载结束
|
||||
drawIn.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
prompt.value = detail.prompt;
|
||||
width.value = detail.width;
|
||||
height.value = detail.height;
|
||||
seed.value = detail.options?.seed;
|
||||
steps.value = detail.options?.steps;
|
||||
scale.value = detail.options?.scale;
|
||||
sampler.value = detail.options?.sampler;
|
||||
clipGuidancePreset.value = detail.options?.clipGuidancePreset;
|
||||
stylePreset.value = detail.options?.stylePreset;
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues });
|
||||
</script>
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<b>画面描述</b>
|
||||
<p>建议使用"形容词 + 动词 + 风格"的格式,使用","隔开</p>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:maxlength="1024"
|
||||
:rows="5"
|
||||
type="textarea"
|
||||
class="mt-4 w-full"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 热词区域 -->
|
||||
<div class="mt-8 flex flex-col">
|
||||
<div><b>随机热词</b></div>
|
||||
<ElSpace wrap class="mt-4 flex flex-wrap gap-2">
|
||||
<ElButton
|
||||
round
|
||||
class="m-0"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotEnglishWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 参数项:采样方法 -->
|
||||
<div class="mt-8">
|
||||
<div><b>采样方法</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElSelect
|
||||
v-model="sampler"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in StableDiffusionSamplers"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- CLIP -->
|
||||
<div class="mt-8">
|
||||
<div><b>CLIP</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElSelect
|
||||
v-model="clipGuidancePreset"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in StableDiffusionClipGuidancePresets"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 风格 -->
|
||||
<div class="mt-8">
|
||||
<div><b>风格</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElSelect
|
||||
v-model="stylePreset"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in StableDiffusionStylePresets"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
>
|
||||
{{ item.name }}
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 图片尺寸 -->
|
||||
<div class="mt-8">
|
||||
<div><b>图片尺寸</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>宽</span>
|
||||
<ElInputNumber
|
||||
v-model="width"
|
||||
placeholder="图片宽度"
|
||||
controls-position="right"
|
||||
class="!w-32"
|
||||
/>
|
||||
<span>px</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>高</span>
|
||||
<ElInputNumber
|
||||
v-model="height"
|
||||
placeholder="图片高度"
|
||||
controls-position="right"
|
||||
class="!w-32"
|
||||
/>
|
||||
<span>px</span>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 迭代步数 -->
|
||||
<div class="mt-8">
|
||||
<div><b>迭代步数</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElInputNumber
|
||||
v-model="steps"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
placeholder="Please input"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 引导系数 -->
|
||||
<div class="mt-8">
|
||||
<div><b>引导系数</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElInputNumber
|
||||
v-model="scale"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
placeholder="Please input"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 随机因子 -->
|
||||
<div class="mt-8">
|
||||
<div><b>随机因子</b></div>
|
||||
<ElSpace wrap class="mt-4 w-full">
|
||||
<ElInputNumber
|
||||
v-model="seed"
|
||||
size="large"
|
||||
class="!w-80"
|
||||
placeholder="Please input"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="mt-12 flex justify-center">
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
round
|
||||
:loading="drawIn"
|
||||
:disabled="prompt.length === 0"
|
||||
@click="handleGenerateImage"
|
||||
>
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
91
apps/web-ele/src/views/ai/image/square/index.vue
Normal file
91
apps/web-ele/src/views/ai/image/square/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { ElImage, ElInput, ElPagination } from 'element-plus';
|
||||
|
||||
import { getImagePageMy } from '#/api/ai/image';
|
||||
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<AiImageApi.Image[]>([]); // 列表的数据
|
||||
const total = ref(0); // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
publicStatus: true,
|
||||
prompt: undefined,
|
||||
});
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getImagePageMy(queryParams);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const debounceGetList = useDebounceFn(getList, 80);
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getList();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="bg-card p-5">
|
||||
<ElInput
|
||||
v-model="queryParams.prompt"
|
||||
class="mb-5 w-full"
|
||||
size="large"
|
||||
placeholder="请输入要搜索的内容"
|
||||
@keyup.enter="handleQuery"
|
||||
>
|
||||
<template #suffix>
|
||||
<IconifyIcon icon="lucide:search" class="cursor-pointer" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<div
|
||||
class="bg-card grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5 shadow-sm"
|
||||
>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="bg-card relative cursor-pointer overflow-hidden transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ElImage
|
||||
:src="item.picUrl"
|
||||
class="block h-auto w-full transition-transform duration-300 hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<ElPagination
|
||||
:total="total"
|
||||
v-model:current-page="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 30, 40, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="debounceGetList"
|
||||
@current-change="debounceGetList"
|
||||
class="mt-5"
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
Reference in New Issue
Block a user