feat(ai): 新增 AI 绘图功能
- 添加 AI 绘图相关的 API 接口和路由 - 实现 AI 绘图页面,支持不同平台的绘图功能 - 添加绘图作品列表和重新生成功能 - 优化绘图页面样式和布局pull/145/head
parent
4596cd9fa5
commit
33b7a11a4e
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
|
|
@ -40,7 +40,7 @@ export namespace AiImageApi {
|
||||||
export interface ImageMidjourneyImagineReqVO {
|
export interface ImageMidjourneyImagineReqVO {
|
||||||
prompt: string; // 提示词
|
prompt: string; // 提示词
|
||||||
modelId: number; // 模型
|
modelId: number; // 模型
|
||||||
base64Array: string[]; // size不能为空
|
base64Array?: string[]; // size不能为空
|
||||||
width: string; // 图片宽度
|
width: string; // 图片宽度
|
||||||
height: string; // 图片高度
|
height: string; // 图片高度
|
||||||
version: string; // 版本
|
version: string; // 版本
|
||||||
|
|
@ -62,7 +62,7 @@ export function getImagePageMy(params: PageParam) {
|
||||||
|
|
||||||
// 获取【我的】绘图记录
|
// 获取【我的】绘图记录
|
||||||
export function getImageMy(id: number) {
|
export function getImageMy(id: number) {
|
||||||
return requestClient.get<AiImageApi.ImageVO[]>(`/ai/image/get-my?id=${id}`);
|
return requestClient.get<AiImageApi.ImageVO>(`/ai/image/get-my?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取【我的】绘图记录列表
|
// 获取【我的】绘图记录列表
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,18 @@ const routes: RouteRecordRaw[] = [
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
},
|
},
|
||||||
children: [
|
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: 'knowledge/document',
|
path: 'knowledge/document',
|
||||||
component: () => import('#/views/ai/knowledge/document/index.vue'),
|
component: () => import('#/views/ai/knowledge/document/index.vue'),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,29 @@ export const AiModelTypeEnum = {
|
||||||
EMBEDDING: 5, // 向量
|
EMBEDDING: 5, // 向量
|
||||||
RERANK: 6, // 重排
|
RERANK: 6, // 重排
|
||||||
};
|
};
|
||||||
|
export interface ImageModelVO {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
export const OtherPlatformEnum: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: AiPlatformEnum.TONG_YI,
|
||||||
|
name: '通义万相',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: AiPlatformEnum.YI_YAN,
|
||||||
|
name: '百度千帆',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: AiPlatformEnum.ZHI_PU,
|
||||||
|
name: '智谱 AI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: AiPlatformEnum.SiliconFlow,
|
||||||
|
name: '硅基流动',
|
||||||
|
},
|
||||||
|
];
|
||||||
/**
|
/**
|
||||||
* AI 图像生成状态的枚举
|
* AI 图像生成状态的枚举
|
||||||
*/
|
*/
|
||||||
|
|
@ -55,6 +77,172 @@ export enum AiWriteTypeEnum {
|
||||||
WRITING = 1, // 撰写
|
WRITING = 1, // 撰写
|
||||||
REPLY, // 回复
|
REPLY, // 回复
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 【图片 UI】相关的枚举 ==========
|
||||||
|
|
||||||
|
export const ImageHotWords = [
|
||||||
|
'中国旗袍',
|
||||||
|
'古装美女',
|
||||||
|
'卡通头像',
|
||||||
|
'机甲战士',
|
||||||
|
'童话小屋',
|
||||||
|
'中国长城',
|
||||||
|
]; // 图片热词
|
||||||
|
|
||||||
|
export const ImageHotEnglishWords = [
|
||||||
|
'Chinese Cheongsam',
|
||||||
|
'Ancient Beauty',
|
||||||
|
'Cartoon Avatar',
|
||||||
|
'Mech Warrior',
|
||||||
|
'Fairy Tale Cottage',
|
||||||
|
'The Great Wall of China',
|
||||||
|
]; // 图片热词(英文)
|
||||||
|
|
||||||
|
export const StableDiffusionSamplers: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'DDIM',
|
||||||
|
name: 'DDIM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'DDPM',
|
||||||
|
name: 'DDPM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_DPMPP_2M',
|
||||||
|
name: 'K_DPMPP_2M',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_DPMPP_2S_ANCESTRAL',
|
||||||
|
name: 'K_DPMPP_2S_ANCESTRAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_DPM_2',
|
||||||
|
name: 'K_DPM_2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_DPM_2_ANCESTRAL',
|
||||||
|
name: 'K_DPM_2_ANCESTRAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_EULER',
|
||||||
|
name: 'K_EULER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_EULER_ANCESTRAL',
|
||||||
|
name: 'K_EULER_ANCESTRAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_HEUN',
|
||||||
|
name: 'K_HEUN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'K_LMS',
|
||||||
|
name: 'K_LMS',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const StableDiffusionStylePresets: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: '3d-model',
|
||||||
|
name: '3d-model',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'analog-film',
|
||||||
|
name: 'analog-film',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'anime',
|
||||||
|
name: 'anime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cinematic',
|
||||||
|
name: 'cinematic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'comic-book',
|
||||||
|
name: 'comic-book',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'digital-art',
|
||||||
|
name: 'digital-art',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enhance',
|
||||||
|
name: 'enhance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fantasy-art',
|
||||||
|
name: 'fantasy-art',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'isometric',
|
||||||
|
name: 'isometric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'line-art',
|
||||||
|
name: 'line-art',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'low-poly',
|
||||||
|
name: 'low-poly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'modeling-compound',
|
||||||
|
name: 'modeling-compound',
|
||||||
|
},
|
||||||
|
// neon-punk origami photographic pixel-art tile-texture
|
||||||
|
{
|
||||||
|
key: 'neon-punk',
|
||||||
|
name: 'neon-punk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'origami',
|
||||||
|
name: 'origami',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'photographic',
|
||||||
|
name: 'photographic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pixel-art',
|
||||||
|
name: 'pixel-art',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tile-texture',
|
||||||
|
name: 'tile-texture',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'NONE',
|
||||||
|
name: 'NONE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'FAST_BLUE',
|
||||||
|
name: 'FAST_BLUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'FAST_GREEN',
|
||||||
|
name: 'FAST_GREEN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'SIMPLE',
|
||||||
|
name: 'SIMPLE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'SLOW',
|
||||||
|
name: 'SLOW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'SLOWER',
|
||||||
|
name: 'SLOWER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'SLOWEST',
|
||||||
|
name: 'SLOWEST',
|
||||||
|
},
|
||||||
|
];
|
||||||
// ========== COMMON 模块 ==========
|
// ========== COMMON 模块 ==========
|
||||||
// 全局通用状态枚举
|
// 全局通用状态枚举
|
||||||
export const CommonStatusEnum = {
|
export const CommonStatusEnum = {
|
||||||
|
|
@ -142,7 +330,136 @@ export const InfraApiErrorLogProcessStatusEnum = {
|
||||||
DONE: 1, // 已处理
|
DONE: 1, // 已处理
|
||||||
IGNORE: 2, // 已忽略
|
IGNORE: 2, // 已忽略
|
||||||
};
|
};
|
||||||
|
export interface ImageSizeVO {
|
||||||
|
key: string;
|
||||||
|
name?: string;
|
||||||
|
style: string;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
}
|
||||||
|
export const Dall3SizeList: ImageSizeVO[] = [
|
||||||
|
{
|
||||||
|
key: '1024x1024',
|
||||||
|
name: '1:1',
|
||||||
|
width: '1024',
|
||||||
|
height: '1024',
|
||||||
|
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '1024x1792',
|
||||||
|
name: '3:5',
|
||||||
|
width: '1024',
|
||||||
|
height: '1792',
|
||||||
|
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '1792x1024',
|
||||||
|
name: '5:3',
|
||||||
|
width: '1792',
|
||||||
|
height: '1024',
|
||||||
|
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Dall3Models: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'dall-e-3',
|
||||||
|
name: 'DALL·E 3',
|
||||||
|
image: `/static/dall2.jpg`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dall-e-2',
|
||||||
|
name: 'DALL·E 2',
|
||||||
|
image: `/static/dall3.jpg`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Dall3StyleList: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'vivid',
|
||||||
|
name: '清晰',
|
||||||
|
image: `/static/qingxi.jpg`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'natural',
|
||||||
|
name: '自然',
|
||||||
|
image: `/static/ziran.jpg`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const MidjourneyModels: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'midjourney',
|
||||||
|
name: 'MJ',
|
||||||
|
image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'niji',
|
||||||
|
name: 'NIJI',
|
||||||
|
image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const MidjourneyVersions = [
|
||||||
|
{
|
||||||
|
value: '6.0',
|
||||||
|
label: 'v6.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '5.2',
|
||||||
|
label: 'v5.2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '5.1',
|
||||||
|
label: 'v5.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '5.0',
|
||||||
|
label: 'v5.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '4.0',
|
||||||
|
label: 'v4.0',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NijiVersionList = [
|
||||||
|
{
|
||||||
|
value: '5',
|
||||||
|
label: 'v5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MidjourneySizeList: ImageSizeVO[] = [
|
||||||
|
{
|
||||||
|
key: '1:1',
|
||||||
|
width: '1',
|
||||||
|
height: '1',
|
||||||
|
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3:4',
|
||||||
|
width: '3',
|
||||||
|
height: '4',
|
||||||
|
style: 'width: 30px; height: 40px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4:3',
|
||||||
|
width: '4',
|
||||||
|
height: '3',
|
||||||
|
style: 'width: 40px; height: 30px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '9:16',
|
||||||
|
width: '9',
|
||||||
|
height: '16',
|
||||||
|
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '16:9',
|
||||||
|
width: '16',
|
||||||
|
height: '9',
|
||||||
|
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
|
||||||
|
},
|
||||||
|
];
|
||||||
// ========== PAY 模块 ==========
|
// ========== PAY 模块 ==========
|
||||||
/**
|
/**
|
||||||
* 支付渠道枚举
|
* 支付渠道枚举
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,79 @@
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间日期转换
|
||||||
|
* @param date 当前时间,new Date() 格式
|
||||||
|
* @param format 需要转换的时间格式字符串
|
||||||
|
* @description format 字符串随意,如 `YYYY-MM、YYYY-MM-DD`
|
||||||
|
* @description format 季度:"YYYY-MM-DD HH:mm:ss QQQQ"
|
||||||
|
* @description format 星期:"YYYY-MM-DD HH:mm:ss WWW"
|
||||||
|
* @description format 几周:"YYYY-MM-DD HH:mm:ss ZZZ"
|
||||||
|
* @description format 季度 + 星期 + 几周:"YYYY-MM-DD HH:mm:ss WWW QQQQ ZZZ"
|
||||||
|
* @returns 返回拼接后的时间字符串
|
||||||
|
*/
|
||||||
|
export function formatDate(date: Date, format?: string): string {
|
||||||
|
// 日期不存在,则返回空
|
||||||
|
if (!date) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// 日期存在,则进行格式化
|
||||||
|
return date ? dayjs(date).format(format ?? 'YYYY-MM-DD HH:mm:ss') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前`
|
||||||
|
* @param param 当前时间,new Date() 格式或者字符串时间格式
|
||||||
|
* @param format 需要转换的时间格式字符串
|
||||||
|
* @description param 10秒: 10 * 1000
|
||||||
|
* @description param 1分: 60 * 1000
|
||||||
|
* @description param 1小时: 60 * 60 * 1000
|
||||||
|
* @description param 24小时:60 * 60 * 24 * 1000
|
||||||
|
* @description param 3天: 60 * 60* 24 * 1000 * 3
|
||||||
|
* @returns 返回拼接后的时间字符串
|
||||||
|
*/
|
||||||
|
export function formatPast(
|
||||||
|
param: Date | string,
|
||||||
|
format = 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
): string {
|
||||||
|
// 传入格式处理、存储转换值
|
||||||
|
let s: number, t: any;
|
||||||
|
// 获取js 时间戳
|
||||||
|
let time: number = Date.now();
|
||||||
|
// 是否是对象
|
||||||
|
typeof param === 'string' || typeof param === 'object'
|
||||||
|
? (t = new Date(param).getTime())
|
||||||
|
: (t = param);
|
||||||
|
// 当前时间戳 - 传入时间戳
|
||||||
|
time = Number.parseInt(`${time - t}`);
|
||||||
|
if (time < 10_000) {
|
||||||
|
// 10秒内
|
||||||
|
return '刚刚';
|
||||||
|
} else if (time < 60_000 && time >= 10_000) {
|
||||||
|
// 超过10秒少于1分钟内
|
||||||
|
s = Math.floor(time / 1000);
|
||||||
|
return `${s}秒前`;
|
||||||
|
} else if (time < 3_600_000 && time >= 60_000) {
|
||||||
|
// 超过1分钟少于1小时
|
||||||
|
s = Math.floor(time / 60_000);
|
||||||
|
return `${s}分钟前`;
|
||||||
|
} else if (time < 86_400_000 && time >= 3_600_000) {
|
||||||
|
// 超过1小时少于24小时
|
||||||
|
s = Math.floor(time / 3_600_000);
|
||||||
|
return `${s}小时前`;
|
||||||
|
} else if (time < 259_200_000 && time >= 86_400_000) {
|
||||||
|
// 超过1天少于3天内
|
||||||
|
s = Math.floor(time / 86_400_000);
|
||||||
|
return `${s}天前`;
|
||||||
|
} else {
|
||||||
|
// 超过3天
|
||||||
|
const date =
|
||||||
|
typeof param === 'string' || typeof param === 'object'
|
||||||
|
? new Date(param)
|
||||||
|
: param;
|
||||||
|
return formatDate(date, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将毫秒,转换成时间字符串。例如说,xx 分钟
|
* 将毫秒,转换成时间字符串。例如说,xx 分钟
|
||||||
*
|
*
|
||||||
|
|
@ -30,3 +106,39 @@ export function formatPast2(ms: number): string {
|
||||||
}
|
}
|
||||||
return second > 0 ? `${second} 秒` : `${0} 秒`;
|
return second > 0 ? `${second} 秒` : `${0} 秒`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date | number | string} time 需要转换的时间
|
||||||
|
* @param {string} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
|
||||||
|
*/
|
||||||
|
export function formatTime(time: Date | number | string, fmt: string) {
|
||||||
|
if (time) {
|
||||||
|
const date = new Date(time);
|
||||||
|
const o = {
|
||||||
|
'M+': date.getMonth() + 1,
|
||||||
|
'd+': date.getDate(),
|
||||||
|
'H+': date.getHours(),
|
||||||
|
'm+': date.getMinutes(),
|
||||||
|
's+': date.getSeconds(),
|
||||||
|
'q+': Math.floor((date.getMonth() + 3) / 3),
|
||||||
|
S: date.getMilliseconds(),
|
||||||
|
};
|
||||||
|
if (/(y+)/.test(fmt)) {
|
||||||
|
fmt = fmt.replace(
|
||||||
|
RegExp.$1,
|
||||||
|
`${date.getFullYear()}`.slice(4 - RegExp.$1.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const k in o) {
|
||||||
|
if (new RegExp(`(${k})`).test(fmt)) {
|
||||||
|
fmt = fmt.replace(
|
||||||
|
RegExp.$1,
|
||||||
|
RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.slice(`${o[k]}`.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export * from './formCreate';
|
||||||
export * from './rangePickerProps';
|
export * from './rangePickerProps';
|
||||||
export * from './routerHelper';
|
export * from './routerHelper';
|
||||||
export * from './upload';
|
export * from './upload';
|
||||||
|
export * from './utils';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Created by 芋道源码
|
||||||
|
*
|
||||||
|
* AI 枚举类
|
||||||
|
*
|
||||||
|
* 问题:为什么不放在 src/utils/common-utils.ts 呢?
|
||||||
|
* 回答:主要 AI 是可选模块,考虑到独立、解耦,所以放在了 /views/ai/utils/common-utils.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 判断字符串是否包含中文 */
|
||||||
|
export const hasChinese = (str: string) => {
|
||||||
|
return /[\u4E00-\u9FA5]/.test(str);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
<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 { Button, Card, Image, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { AiImageStatusEnum } from '#/utils/constants';
|
||||||
|
|
||||||
|
// 消息
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
detail: {
|
||||||
|
type: Object as PropType<AiImageApi.ImageVO>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']);
|
||||||
|
|
||||||
|
const cardImageRef = ref<any>(); // 卡片 image ref
|
||||||
|
|
||||||
|
/** 处理点击事件 */
|
||||||
|
const handleButtonClick = async (type: string, detail: AiImageApi.ImageVO) => {
|
||||||
|
emits('onBtnClick', type, detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理 Midjourney 按钮点击事件 */
|
||||||
|
const handleMidjourneyBtnClick = async (
|
||||||
|
button: AiImageApi.ImageMidjourneyButtonsVO,
|
||||||
|
) => {
|
||||||
|
// 确认窗体
|
||||||
|
await confirm(`确认操作 "${button.label} ${button.emoji}" ?`);
|
||||||
|
emits('onMjBtnClick', button, props.detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
// emits
|
||||||
|
|
||||||
|
/** 监听详情 */
|
||||||
|
const { detail } = toRefs(props);
|
||||||
|
watch(detail, async (newVal) => {
|
||||||
|
await handleLoading(newVal.status);
|
||||||
|
});
|
||||||
|
const loading = ref();
|
||||||
|
/** 处理加载状态 */
|
||||||
|
const handleLoading = async (status: number) => {
|
||||||
|
// 情况一:如果是生成中,则设置加载中的 loading
|
||||||
|
if (status === AiImageStatusEnum.IN_PROGRESS) {
|
||||||
|
loading.value = message.loading({
|
||||||
|
content: `生成中...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 情况二:如果已经生成结束,则移除 loading
|
||||||
|
} else {
|
||||||
|
if (loading.value) setTimeout(loading.value, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await handleLoading(props.detail.status);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Card body-class="" class="image-card">
|
||||||
|
<div class="image-operation">
|
||||||
|
<div>
|
||||||
|
<Button v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
|
||||||
|
生成中
|
||||||
|
</Button>
|
||||||
|
<Button v-else-if="detail?.status === AiImageStatusEnum.SUCCESS">
|
||||||
|
已完成
|
||||||
|
</Button>
|
||||||
|
<Button danger v-else-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||||
|
异常
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<!-- 操作区 -->
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
class="btn"
|
||||||
|
type="text"
|
||||||
|
@click="handleButtonClick('download', detail)"
|
||||||
|
>
|
||||||
|
<span class="icon-[ant-design--download-outlined]"></span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="btn"
|
||||||
|
type="text"
|
||||||
|
@click="handleButtonClick('regeneration', detail)"
|
||||||
|
>
|
||||||
|
<span class="icon-[ant-design--redo-outlined]"></span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="btn"
|
||||||
|
type="text"
|
||||||
|
@click="handleButtonClick('delete', detail)"
|
||||||
|
>
|
||||||
|
<span class="icon-[ant-design--delete-outlined]"></span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="btn"
|
||||||
|
type="text"
|
||||||
|
@click="handleButtonClick('more', detail)"
|
||||||
|
>
|
||||||
|
<span class="icon-[ant-design--more-outlined]"></span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-wrapper" ref="cardImageRef">
|
||||||
|
<Image class="image" :src="detail?.picUrl" />
|
||||||
|
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||||
|
{{ detail?.errorMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Midjourney 专属操作 -->
|
||||||
|
<div class="image-mj-btns">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
v-for="(button, index) in detail?.buttons"
|
||||||
|
:key="index"
|
||||||
|
class="ml-0 mr-[10px] mt-[5px] min-w-[40px]"
|
||||||
|
@click="handleMidjourneyBtnClick(button)"
|
||||||
|
>
|
||||||
|
{{ button.label }}{{ button.emoji }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.image-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 320px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
.image-operation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
//border: 1px solid red;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
height: 280px;
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-mj-btns {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiImageApi } from '#/api/ai/image';
|
||||||
|
import type { ImageModelVO } from '#/utils/constants';
|
||||||
|
|
||||||
|
import { ref, toRefs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getImageMy } from '#/api/ai/image';
|
||||||
|
import {
|
||||||
|
AiPlatformEnum,
|
||||||
|
Dall3StyleList,
|
||||||
|
StableDiffusionClipGuidancePresets,
|
||||||
|
StableDiffusionSamplers,
|
||||||
|
StableDiffusionStylePresets,
|
||||||
|
} from '#/utils/constants';
|
||||||
|
import { formatTime } from '#/utils/formatTime';
|
||||||
|
|
||||||
|
// 图片详细信息
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const detail = ref<AiImageApi.ImageVO>({} as AiImageApi.ImageVO);
|
||||||
|
|
||||||
|
/** 获取图片详情 */
|
||||||
|
const getImageDetail = async (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="item">
|
||||||
|
<div class="body">
|
||||||
|
<Image class="image" :src="detail?.picUrl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 时间 -->
|
||||||
|
<div class="item">
|
||||||
|
<div class="tip">时间</div>
|
||||||
|
<div class="body">
|
||||||
|
<div>
|
||||||
|
提交时间:{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
生成时间:{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 模型 -->
|
||||||
|
<div class="item">
|
||||||
|
<div class="tip">模型</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail.model }}({{ detail.height }}x{{ detail.width }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 提示词 -->
|
||||||
|
<div class="item">
|
||||||
|
<div class="tip">提示词</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail.prompt }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 地址 -->
|
||||||
|
<div class="item">
|
||||||
|
<div class="tip">图片地址</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail.picUrl }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- StableDiffusion 专属区域 -->
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||||
|
detail?.options?.sampler
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">采样方法</div>
|
||||||
|
<div class="body">
|
||||||
|
{{
|
||||||
|
StableDiffusionSamplers.find(
|
||||||
|
(item: ImageModelVO) => item.key === detail?.options?.sampler,
|
||||||
|
)?.name
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||||
|
detail?.options?.clipGuidancePreset
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">CLIP</div>
|
||||||
|
<div class="body">
|
||||||
|
{{
|
||||||
|
StableDiffusionClipGuidancePresets.find(
|
||||||
|
(item: ImageModelVO) =>
|
||||||
|
item.key === detail?.options?.clipGuidancePreset,
|
||||||
|
)?.name
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||||
|
detail?.options?.stylePreset
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">风格</div>
|
||||||
|
<div class="body">
|
||||||
|
{{
|
||||||
|
StableDiffusionStylePresets.find(
|
||||||
|
(item: ImageModelVO) => item.key === detail?.options?.stylePreset,
|
||||||
|
)?.name
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||||
|
detail?.options?.steps
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">迭代步数</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail?.options?.steps }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||||
|
detail?.options?.scale
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">引导系数</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail?.options?.scale }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.STABLE_DIFFUSION &&
|
||||||
|
detail?.options?.seed
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">随机因子</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail?.options?.seed }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Dall3 专属区域 -->
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"
|
||||||
|
>
|
||||||
|
<div class="tip">风格选择</div>
|
||||||
|
<div class="body">
|
||||||
|
{{
|
||||||
|
Dall3StyleList.find(
|
||||||
|
(item: ImageModelVO) => item.key === detail?.options?.style,
|
||||||
|
)?.name
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Midjourney 专属区域 -->
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">模型版本</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ detail?.options?.version }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-if="
|
||||||
|
detail.platform === AiPlatformEnum.MIDJOURNEY &&
|
||||||
|
detail?.options?.referImageUrl
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="tip">参考图</div>
|
||||||
|
<div class="body">
|
||||||
|
<Image :src="detail.options.referImageUrl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #616161;
|
||||||
|
|
||||||
|
.taskImage {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
<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 { useDebounceFn } from '@vueuse/core';
|
||||||
|
import { Button, Card, message, Pagination } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteImageMy,
|
||||||
|
getImageListMyByIds,
|
||||||
|
getImagePageMy,
|
||||||
|
midjourneyAction,
|
||||||
|
} from '#/api/ai/image';
|
||||||
|
import { AiImageStatusEnum } from '#/utils/constants';
|
||||||
|
import { download } from '#/utils/download';
|
||||||
|
|
||||||
|
import ImageCard from './ImageCard.vue';
|
||||||
|
import ImageDetail from './ImageDetail.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.ImageVO[]>([]); // image 列表
|
||||||
|
const imageListRef = ref<any>(); // ref
|
||||||
|
// 图片轮询相关的参数(正在生成中的)
|
||||||
|
const inProgressImageMap = ref<{}>({}); // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||||
|
const inProgressTimer = ref<any>(); // 生成中的 image 定时器,轮询生成进展
|
||||||
|
const showImageDetailId = ref<number>(0); // 图片详情的图片编号
|
||||||
|
|
||||||
|
/** 处理查看绘图作品 */
|
||||||
|
const handleViewPublic = () => {
|
||||||
|
router.push({
|
||||||
|
name: 'AiImageSquare',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查看图片的详情 */
|
||||||
|
const handleDetailOpen = async () => {
|
||||||
|
drawerApi.open();
|
||||||
|
};
|
||||||
|
/** 获得 image 图片列表 */
|
||||||
|
const getImageList = async () => {
|
||||||
|
const loading = message.loading({
|
||||||
|
content: `加载中...`,
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const debounceGetImageList = useDebounceFn(getImageList, 80);
|
||||||
|
/** 轮询生成中的 image 列表 */
|
||||||
|
const refreshWatchImages = async () => {
|
||||||
|
const imageIds = Object.keys(inProgressImageMap.value).map(Number);
|
||||||
|
if (imageIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (await getImageListMyByIds(imageIds)) as AiImageApi.ImageVO[];
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图片的点击事件 */
|
||||||
|
const handleImageButtonClick = async (
|
||||||
|
type: string,
|
||||||
|
imageDetail: AiImageApi.ImageVO,
|
||||||
|
) => {
|
||||||
|
// 详情
|
||||||
|
if (type === 'more') {
|
||||||
|
showImageDetailId.value = imageDetail.id;
|
||||||
|
await handleDetailOpen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 删除
|
||||||
|
if (type === 'delete') {
|
||||||
|
await confirm(`是否删除照片?`);
|
||||||
|
await deleteImageMy(imageDetail.id);
|
||||||
|
await getImageList();
|
||||||
|
message.success('删除成功!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 下载
|
||||||
|
if (type === 'download') {
|
||||||
|
await download.image({ url: imageDetail.picUrl });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 重新生成
|
||||||
|
if (type === 'regeneration') {
|
||||||
|
await emits('onRegeneration', imageDetail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理 Midjourney 按钮点击事件 */
|
||||||
|
const handleImageMidjourneyButtonClick = async (
|
||||||
|
button: AiImageApi.ImageMidjourneyButtonsVO,
|
||||||
|
imageDetail: AiImageApi.ImageVO,
|
||||||
|
) => {
|
||||||
|
// 1. 构建 params 参数
|
||||||
|
const data = {
|
||||||
|
id: imageDetail.id,
|
||||||
|
customId: button.customId,
|
||||||
|
} as AiImageApi.ImageMidjourneyActionVO;
|
||||||
|
// 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-[600px]">
|
||||||
|
<ImageDetail :id="showImageDetailId" />
|
||||||
|
</Drawer>
|
||||||
|
<Card
|
||||||
|
class="dr-task"
|
||||||
|
:body-style="{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
绘画任务
|
||||||
|
<!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
|
||||||
|
<Button @click="handleViewPublic">绘画作品</Button>
|
||||||
|
</template>
|
||||||
|
<div class="task-image-list" ref="imageListRef">
|
||||||
|
<ImageCard
|
||||||
|
v-for="image in imageList"
|
||||||
|
:key="image.id"
|
||||||
|
:detail="image"
|
||||||
|
@on-btn-click="handleImageButtonClick"
|
||||||
|
@on-mj-btn-click="handleImageMidjourneyButtonClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="task-image-pagination">
|
||||||
|
<Pagination
|
||||||
|
:total="pageTotal"
|
||||||
|
:show-total="(total: number) => `共 ${total} 条`"
|
||||||
|
show-quick-jumper
|
||||||
|
show-size-changer
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
@change="debounceGetImageList"
|
||||||
|
@show-size-change="debounceGetImageList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.dr-task {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-image-list {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box; /* 确保内边距不会增加高度 */
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px 20px 140px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:last-of-type {
|
||||||
|
//margin-bottom: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-image-pagination {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 90px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
<!-- 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 { Button, InputNumber, Select, Space, Textarea } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { drawImage } from '#/api/ai/image';
|
||||||
|
import {
|
||||||
|
AiPlatformEnum,
|
||||||
|
ImageHotWords,
|
||||||
|
OtherPlatformEnum,
|
||||||
|
} from '#/utils/constants';
|
||||||
|
|
||||||
|
// 消息弹窗
|
||||||
|
|
||||||
|
// 接收父组件传入的模型列表
|
||||||
|
const props = defineProps({
|
||||||
|
models: {
|
||||||
|
type: Array<AiModelModelApi.ModelVO>,
|
||||||
|
default: () => [] as AiModelModelApi.ModelVO[],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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.ModelVO[]>([]); // 模型列表
|
||||||
|
const modelId = ref<number>(); // 选中的模型
|
||||||
|
|
||||||
|
/** 选择热词 */
|
||||||
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
|
// 情况一:取消选中
|
||||||
|
if (selectHotWord.value === hotWord) {
|
||||||
|
selectHotWord.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况二:选中
|
||||||
|
selectHotWord.value = hotWord; // 选中
|
||||||
|
prompt.value = hotWord; // 替换提示词
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图片生成 */
|
||||||
|
const handleGenerateImage = async () => {
|
||||||
|
// 二次确认
|
||||||
|
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.ImageDrawReqVO;
|
||||||
|
await drawImage(form);
|
||||||
|
} finally {
|
||||||
|
// 回调
|
||||||
|
emits('onDrawComplete', otherPlatform.value);
|
||||||
|
// 加载结束
|
||||||
|
drawIn.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 填充值 */
|
||||||
|
const settingValues = async (detail: AiImageApi.ImageVO) => {
|
||||||
|
prompt.value = detail.prompt;
|
||||||
|
width.value = detail.width;
|
||||||
|
height.value = detail.height;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 平台切换 */
|
||||||
|
const handlerPlatformChange = async (platform: any) => {
|
||||||
|
// 根据选择的平台筛选模型
|
||||||
|
platformModels.value = props.models.filter(
|
||||||
|
(item: AiModelModelApi.ModelVO) => 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>
|
||||||
|
<Textarea
|
||||||
|
v-model:value="prompt"
|
||||||
|
:maxlength="1024"
|
||||||
|
:rows="5"
|
||||||
|
class="w-100% mt-[15px]"
|
||||||
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hot-words">
|
||||||
|
<div>
|
||||||
|
<b>随机热词</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="word-list">
|
||||||
|
<Button
|
||||||
|
shape="round"
|
||||||
|
class="btn"
|
||||||
|
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||||
|
v-for="hotWord in ImageHotWords"
|
||||||
|
:key="hotWord"
|
||||||
|
@click="handleHotWordClick(hotWord)"
|
||||||
|
>
|
||||||
|
{{ hotWord }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>平台</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<Select
|
||||||
|
v-model:value="otherPlatform"
|
||||||
|
placeholder="Select"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
@change="handlerPlatformChange"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="item in OtherPlatformEnum"
|
||||||
|
:key="item.key"
|
||||||
|
:value="item.key"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>模型</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<Select
|
||||||
|
v-model:value="modelId"
|
||||||
|
placeholder="Select"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="item in platformModels"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>图片尺寸</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="width"
|
||||||
|
class="mt-[10px] w-[170px]"
|
||||||
|
placeholder="图片宽度"
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="height"
|
||||||
|
class="w-[170px]"
|
||||||
|
placeholder="图片高度"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
:loading="drawIn"
|
||||||
|
:disabled="prompt.length === 0"
|
||||||
|
@click="handleGenerateImage"
|
||||||
|
>
|
||||||
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hot-words {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: start;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型
|
||||||
|
.group-item {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.group-item-body {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,389 @@
|
||||||
|
<!-- dall3 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiImageApi } from '#/api/ai/image';
|
||||||
|
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||||
|
import type { ImageModelVO, ImageSizeVO } from '#/utils/constants';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Image, message, Space, Textarea } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { drawImage } from '#/api/ai/image';
|
||||||
|
import {
|
||||||
|
AiPlatformEnum,
|
||||||
|
Dall3Models,
|
||||||
|
Dall3SizeList,
|
||||||
|
Dall3StyleList,
|
||||||
|
ImageHotWords,
|
||||||
|
} from '#/utils/constants';
|
||||||
|
|
||||||
|
// 接收父组件传入的模型列表
|
||||||
|
const props = defineProps({
|
||||||
|
models: {
|
||||||
|
type: Array<AiModelModelApi.ModelVO>,
|
||||||
|
default: () => [] as AiModelModelApi.ModelVO[],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 样式
|
||||||
|
|
||||||
|
/** 选择热词 */
|
||||||
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
|
// 情况一:取消选中
|
||||||
|
if (selectHotWord.value === hotWord) {
|
||||||
|
selectHotWord.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况二:选中
|
||||||
|
selectHotWord.value = hotWord;
|
||||||
|
prompt.value = hotWord;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 选择 model 模型 */
|
||||||
|
const handleModelClick = async (model: ImageModelVO) => {
|
||||||
|
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 样式 */
|
||||||
|
const handleStyleClick = async (imageStyle: ImageModelVO) => {
|
||||||
|
style.value = imageStyle.key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 选择 size 大小 */
|
||||||
|
const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
||||||
|
selectSize.value = imageSize.key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图片生产 */
|
||||||
|
const handleGenerateImage = async () => {
|
||||||
|
// 从 models 中查找匹配的模型
|
||||||
|
const matchedModel = props.models.find(
|
||||||
|
(item) =>
|
||||||
|
item.model === selectModel.value &&
|
||||||
|
item.platform === AiPlatformEnum.OPENAI,
|
||||||
|
);
|
||||||
|
if (!matchedModel) {
|
||||||
|
message.error('该模型不可用,请选择其它模型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次确认
|
||||||
|
await confirm(`确认生成内容?`);
|
||||||
|
try {
|
||||||
|
// 加载中
|
||||||
|
drawIn.value = true;
|
||||||
|
// 回调
|
||||||
|
emits('onDrawStart', AiPlatformEnum.OPENAI);
|
||||||
|
const imageSize = Dall3SizeList.find(
|
||||||
|
(item) => item.key === selectSize.value,
|
||||||
|
) as ImageSizeVO;
|
||||||
|
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.ImageDrawReqVO;
|
||||||
|
// 发送请求
|
||||||
|
await drawImage(form);
|
||||||
|
} finally {
|
||||||
|
// 回调
|
||||||
|
emits('onDrawComplete', AiPlatformEnum.OPENAI);
|
||||||
|
// 加载结束
|
||||||
|
drawIn.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 填充值 */
|
||||||
|
const settingValues = async (detail: AiImageApi.ImageVO) => {
|
||||||
|
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 ImageSizeVO;
|
||||||
|
await handleSizeClick(imageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 暴露组件方法 */
|
||||||
|
defineExpose({ settingValues });
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="prompt">
|
||||||
|
<b>画面描述</b>
|
||||||
|
<p>建议使用"形容词 + 动词 + 风格"的格式,使用","隔开</p>
|
||||||
|
<Textarea
|
||||||
|
v-model:value="prompt"
|
||||||
|
:maxlength="1024"
|
||||||
|
:rows="5"
|
||||||
|
class="w-100% mt-[15px]"
|
||||||
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hot-words">
|
||||||
|
<div>
|
||||||
|
<b>随机热词</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="word-list">
|
||||||
|
<Button
|
||||||
|
shape="round"
|
||||||
|
class="btn"
|
||||||
|
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||||
|
v-for="hotWord in ImageHotWords"
|
||||||
|
:key="hotWord"
|
||||||
|
@click="handleHotWordClick(hotWord)"
|
||||||
|
>
|
||||||
|
{{ hotWord }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="model">
|
||||||
|
<div>
|
||||||
|
<b>模型选择</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="model-list">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
selectModel === model.key ? 'modal-item selectModel' : 'modal-item'
|
||||||
|
"
|
||||||
|
v-for="model in Dall3Models"
|
||||||
|
:key="model.key"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
:preview="false"
|
||||||
|
:src="model.image"
|
||||||
|
fit="contain"
|
||||||
|
@click="handleModelClick(model)"
|
||||||
|
/>
|
||||||
|
<div class="model-font">{{ model.name }}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="image-style">
|
||||||
|
<div>
|
||||||
|
<b>风格选择</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="image-style-list">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
style === imageStyle.key
|
||||||
|
? 'image-style-item selectImageStyle'
|
||||||
|
: 'image-style-item'
|
||||||
|
"
|
||||||
|
v-for="imageStyle in Dall3StyleList"
|
||||||
|
:key="imageStyle.key"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
:preview="false"
|
||||||
|
:src="imageStyle.image"
|
||||||
|
fit="contain"
|
||||||
|
@click="handleStyleClick(imageStyle)"
|
||||||
|
/>
|
||||||
|
<div class="style-font">{{ imageStyle.name }}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="image-size">
|
||||||
|
<div>
|
||||||
|
<b>画面比例</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="size-list">
|
||||||
|
<div
|
||||||
|
class="size-item"
|
||||||
|
v-for="imageSize in Dall3SizeList"
|
||||||
|
:key="imageSize.key"
|
||||||
|
@click="handleSizeClick(imageSize)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
selectSize === imageSize.key
|
||||||
|
? 'size-wrapper selectImageSize'
|
||||||
|
: 'size-wrapper'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div :style="imageSize.style"></div>
|
||||||
|
</div>
|
||||||
|
<div class="size-font">{{ imageSize.name }}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
:loading="drawIn"
|
||||||
|
:disabled="prompt.length === 0"
|
||||||
|
@click="handleGenerateImage"
|
||||||
|
>
|
||||||
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// 热词
|
||||||
|
.hot-words {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: start;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型
|
||||||
|
.model {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.model-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.modal-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 110px;
|
||||||
|
//outline: 1px solid blue;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
|
||||||
|
.model-font {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectModel {
|
||||||
|
border: 3px solid #1293ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 样式 style
|
||||||
|
.image-style {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.image-style-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.image-style-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 110px;
|
||||||
|
//outline: 1px solid blue;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
|
||||||
|
.style-font {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectImageStyle {
|
||||||
|
border: 3px solid #1293ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尺寸
|
||||||
|
.image-size {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.size-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.size-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.size-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-font {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectImageSize {
|
||||||
|
border: 1px solid #1293ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
<!-- dall3 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiImageApi } from '#/api/ai/image';
|
||||||
|
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||||
|
import type { ImageModelVO, ImageSizeVO } from '#/utils/constants';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Image,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Textarea,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { midjourneyImagine } from '#/api/ai/image';
|
||||||
|
import { ImageUpload } from '#/components/upload';
|
||||||
|
import {
|
||||||
|
AiPlatformEnum,
|
||||||
|
ImageHotWords,
|
||||||
|
MidjourneyModels,
|
||||||
|
MidjourneySizeList,
|
||||||
|
MidjourneyVersions,
|
||||||
|
NijiVersionList,
|
||||||
|
} from '#/utils/constants';
|
||||||
|
|
||||||
|
// 消息弹窗
|
||||||
|
|
||||||
|
// 接收父组件传入的模型列表
|
||||||
|
const props = defineProps({
|
||||||
|
models: {
|
||||||
|
type: Array<AiModelModelApi.ModelVO>,
|
||||||
|
default: () => [] as AiModelModelApi.ModelVO[],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 列表
|
||||||
|
|
||||||
|
/** 选择热词 */
|
||||||
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
|
// 情况一:取消选中
|
||||||
|
if (selectHotWord.value === hotWord) {
|
||||||
|
selectHotWord.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况二:选中
|
||||||
|
selectHotWord.value = hotWord; // 选中
|
||||||
|
prompt.value = hotWord; // 设置提示次
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 点击 size 尺寸 */
|
||||||
|
const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
||||||
|
selectSize.value = imageSize.key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 点击 model 模型 */
|
||||||
|
const handleModelClick = async (model: ImageModelVO) => {
|
||||||
|
selectModel.value = model.key;
|
||||||
|
versionList.value =
|
||||||
|
model.key === 'niji' ? NijiVersionList : MidjourneyVersions;
|
||||||
|
selectVersion.value = versionList.value[0].value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图片生成 */
|
||||||
|
const handleGenerateImage = async () => {
|
||||||
|
// 从 models 中查找匹配的模型
|
||||||
|
const matchedModel = props.models.find(
|
||||||
|
(item) =>
|
||||||
|
item.model === selectModel.value &&
|
||||||
|
item.platform === AiPlatformEnum.MIDJOURNEY,
|
||||||
|
);
|
||||||
|
if (!matchedModel) {
|
||||||
|
message.error('该模型不可用,请选择其它模型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次确认
|
||||||
|
await confirm(`确认生成内容?`);
|
||||||
|
try {
|
||||||
|
// 加载中
|
||||||
|
drawIn.value = true;
|
||||||
|
// 回调
|
||||||
|
emits('onDrawStart', AiPlatformEnum.MIDJOURNEY);
|
||||||
|
// 发送请求
|
||||||
|
const imageSize = MidjourneySizeList.find(
|
||||||
|
(item) => selectSize.value === item.key,
|
||||||
|
) as ImageSizeVO;
|
||||||
|
const req = {
|
||||||
|
prompt: prompt.value,
|
||||||
|
modelId: matchedModel.id,
|
||||||
|
width: imageSize.width,
|
||||||
|
height: imageSize.height,
|
||||||
|
version: selectVersion.value,
|
||||||
|
referImageUrl: referImageUrl.value,
|
||||||
|
} as AiImageApi.ImageMidjourneyImagineReqVO;
|
||||||
|
await midjourneyImagine(req);
|
||||||
|
} finally {
|
||||||
|
// 回调
|
||||||
|
emits('onDrawComplete', AiPlatformEnum.MIDJOURNEY);
|
||||||
|
// 加载结束
|
||||||
|
drawIn.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 填充值 */
|
||||||
|
const settingValues = async (detail: AiImageApi.ImageVO) => {
|
||||||
|
// 提示词
|
||||||
|
prompt.value = detail.prompt;
|
||||||
|
// image size
|
||||||
|
const imageSize = MidjourneySizeList.find(
|
||||||
|
(item) => item.key === `${detail.width}:${detail.height}`,
|
||||||
|
) as ImageSizeVO;
|
||||||
|
selectSize.value = imageSize.key;
|
||||||
|
// 选中模型
|
||||||
|
const model = MidjourneyModels.find(
|
||||||
|
(item) => item.key === detail.options?.model,
|
||||||
|
) as ImageModelVO;
|
||||||
|
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>
|
||||||
|
<Textarea
|
||||||
|
v-model:value="prompt"
|
||||||
|
:maxlength="1024"
|
||||||
|
:rows="5"
|
||||||
|
class="w-100% mt-[15px]"
|
||||||
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hot-words">
|
||||||
|
<div>
|
||||||
|
<b>随机热词</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="word-list">
|
||||||
|
<Button
|
||||||
|
shape="round"
|
||||||
|
class="btn"
|
||||||
|
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||||
|
v-for="hotWord in ImageHotWords"
|
||||||
|
:key="hotWord"
|
||||||
|
@click="handleHotWordClick(hotWord)"
|
||||||
|
>
|
||||||
|
{{ hotWord }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="image-size">
|
||||||
|
<div>
|
||||||
|
<b>尺寸</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="size-list">
|
||||||
|
<div
|
||||||
|
class="size-item"
|
||||||
|
v-for="imageSize in MidjourneySizeList"
|
||||||
|
:key="imageSize.key"
|
||||||
|
@click="handleSizeClick(imageSize)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
selectSize === imageSize.key
|
||||||
|
? 'size-wrapper selectImageSize'
|
||||||
|
: 'size-wrapper'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div :style="imageSize.style"></div>
|
||||||
|
</div>
|
||||||
|
<div class="size-font">{{ imageSize.key }}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="model">
|
||||||
|
<div>
|
||||||
|
<b>模型</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="model-list">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
selectModel === model.key ? 'modal-item selectModel' : 'modal-item'
|
||||||
|
"
|
||||||
|
v-for="model in MidjourneyModels"
|
||||||
|
:key="model.key"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
:preview="false"
|
||||||
|
:src="model.image"
|
||||||
|
fit="contain"
|
||||||
|
@click="handleModelClick(model)"
|
||||||
|
/>
|
||||||
|
<div class="model-font">{{ model.name }}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="version">
|
||||||
|
<div>
|
||||||
|
<b>版本</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="version-list">
|
||||||
|
<Select
|
||||||
|
v-model:value="selectVersion"
|
||||||
|
class="version-select !w-[330px]"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择版本"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="item in versionList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="model">
|
||||||
|
<div>
|
||||||
|
<b>参考图</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="model-list">
|
||||||
|
<ImageUpload v-model:value="referImageUrl" :show-description="false" />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
:disabled="prompt.length === 0"
|
||||||
|
@click="handleGenerateImage"
|
||||||
|
>
|
||||||
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// 热词
|
||||||
|
.hot-words {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: start;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// version
|
||||||
|
.version {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.version-list {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型
|
||||||
|
.model {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.model-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.modal-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 150px;
|
||||||
|
//outline: 1px solid blue;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
|
||||||
|
.model-font {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectModel {
|
||||||
|
border: 3px solid #1293ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尺寸
|
||||||
|
.image-size {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.size-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.size-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.size-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-font {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectImageSize {
|
||||||
|
border: 1px solid #1293ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
<!-- 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 {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Textarea,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { drawImage } from '#/api/ai/image';
|
||||||
|
import {
|
||||||
|
AiPlatformEnum,
|
||||||
|
ImageHotEnglishWords,
|
||||||
|
StableDiffusionClipGuidancePresets,
|
||||||
|
StableDiffusionSamplers,
|
||||||
|
StableDiffusionStylePresets,
|
||||||
|
} from '#/utils/constants';
|
||||||
|
import { hasChinese } from '#/utils/utils';
|
||||||
|
|
||||||
|
// 消息弹窗
|
||||||
|
|
||||||
|
// 接收父组件传入的模型列表
|
||||||
|
const props = defineProps({
|
||||||
|
models: {
|
||||||
|
type: Array<AiModelModelApi.ModelVO>,
|
||||||
|
default: () => [] as AiModelModelApi.ModelVO[],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 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'); // 风格
|
||||||
|
|
||||||
|
/** 选择热词 */
|
||||||
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
|
// 情况一:取消选中
|
||||||
|
if (selectHotWord.value === hotWord) {
|
||||||
|
selectHotWord.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况二:选中
|
||||||
|
selectHotWord.value = hotWord; // 选中
|
||||||
|
prompt.value = hotWord; // 替换提示词
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图片生成 */
|
||||||
|
const handleGenerateImage = async () => {
|
||||||
|
// 从 models 中查找匹配的模型
|
||||||
|
const selectModel = 'stable-diffusion-v1-6';
|
||||||
|
const matchedModel = props.models.find(
|
||||||
|
(item) =>
|
||||||
|
item.model === selectModel &&
|
||||||
|
item.platform === AiPlatformEnum.STABLE_DIFFUSION,
|
||||||
|
);
|
||||||
|
if (!matchedModel) {
|
||||||
|
message.error('该模型不可用,请选择其它模型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次确认
|
||||||
|
if (hasChinese(prompt.value)) {
|
||||||
|
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.ImageDrawReqVO;
|
||||||
|
await drawImage(form);
|
||||||
|
} finally {
|
||||||
|
// 回调
|
||||||
|
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION);
|
||||||
|
// 加载结束
|
||||||
|
drawIn.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 填充值 */
|
||||||
|
const settingValues = async (detail: AiImageApi.ImageVO) => {
|
||||||
|
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>
|
||||||
|
<Textarea
|
||||||
|
v-model:value="prompt"
|
||||||
|
:maxlength="1024"
|
||||||
|
:rows="5"
|
||||||
|
class="w-100% mt-[15px]"
|
||||||
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hot-words">
|
||||||
|
<div>
|
||||||
|
<b>随机热词</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="word-list">
|
||||||
|
<Button
|
||||||
|
shape="round"
|
||||||
|
class="btn"
|
||||||
|
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||||
|
v-for="hotWord in ImageHotEnglishWords"
|
||||||
|
:key="hotWord"
|
||||||
|
@click="handleHotWordClick(hotWord)"
|
||||||
|
>
|
||||||
|
{{ hotWord }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>采样方法</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<Select
|
||||||
|
v-model:value="sampler"
|
||||||
|
placeholder="Select"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="item in StableDiffusionSamplers"
|
||||||
|
:key="item.key"
|
||||||
|
:value="item.key"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>CLIP</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<Select
|
||||||
|
v-model:value="clipGuidancePreset"
|
||||||
|
placeholder="Select"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="item in StableDiffusionClipGuidancePresets"
|
||||||
|
:key="item.key"
|
||||||
|
:value="item.key"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>风格</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<Select
|
||||||
|
v-model:value="stylePreset"
|
||||||
|
placeholder="Select"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="item in StableDiffusionStylePresets"
|
||||||
|
:key="item.key"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.key"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>图片尺寸</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<Input v-model="width" class="w-[170px]" placeholder="图片宽度" />
|
||||||
|
<Input v-model="height" class="w-[170px]" placeholder="图片高度" />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>迭代步数</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<InputNumber
|
||||||
|
v-model="steps"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
placeholder="Please input"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>引导系数</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<InputNumber
|
||||||
|
v-model="scale"
|
||||||
|
type="number"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
placeholder="Please input"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="group-item">
|
||||||
|
<div>
|
||||||
|
<b>随机因子</b>
|
||||||
|
</div>
|
||||||
|
<Space wrap class="group-item-body">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="seed"
|
||||||
|
size="large"
|
||||||
|
class="!w-[330px]"
|
||||||
|
placeholder="Please input"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
:loading="drawIn"
|
||||||
|
:disabled="prompt.length === 0"
|
||||||
|
@click="handleGenerateImage"
|
||||||
|
>
|
||||||
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// 热词
|
||||||
|
.hot-words {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: start;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型
|
||||||
|
.group-item {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.group-item-body {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,28 +1,171 @@
|
||||||
<script lang="ts" setup>
|
<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 { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { Segmented } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getModelSimpleList } from '#/api/ai/model/model';
|
||||||
|
import { AiModelTypeEnum, AiPlatformEnum } from '#/utils/constants';
|
||||||
|
|
||||||
|
import Common from './components/common/index.vue';
|
||||||
|
import Dall3 from './components/dall3/index.vue';
|
||||||
|
import ImageList from './components/ImageList.vue';
|
||||||
|
import Midjourney from './components/midjourney/index.vue';
|
||||||
|
import StableDiffusion from './components/stableDiffusion/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.ModelVO[]>([]); // 模型列表
|
||||||
|
|
||||||
|
/** 绘画 start */
|
||||||
|
const handleDrawStart = async () => {};
|
||||||
|
|
||||||
|
/** 绘画 complete */
|
||||||
|
const handleDrawComplete = async () => {
|
||||||
|
await imageListRef.value.getImageList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重新生成:将画图详情填充到对应平台 */
|
||||||
|
const handleRegeneration = async (image: AiImageApi.ImageVO) => {
|
||||||
|
// 切换平台
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<Button
|
<div class="ai-image">
|
||||||
danger
|
<div class="left">
|
||||||
type="link"
|
<div class="segmented flex justify-center">
|
||||||
target="_blank"
|
<Segmented
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
v-model:value="selectPlatform"
|
||||||
>
|
:options="platformOptions"
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
/>
|
||||||
</Button>
|
</div>
|
||||||
<br />
|
<div class="modal-switch-container">
|
||||||
<Button
|
<Common
|
||||||
type="link"
|
v-if="selectPlatform === 'common'"
|
||||||
target="_blank"
|
ref="commonRef"
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/image/index/index.vue"
|
:models="models"
|
||||||
>
|
@on-draw-complete="handleDrawComplete"
|
||||||
可参考
|
/>
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/image/index/index.vue
|
<Dall3
|
||||||
代码,pull request 贡献给我们!
|
v-if="selectPlatform === AiPlatformEnum.OPENAI"
|
||||||
</Button>
|
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="main">
|
||||||
|
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.ai-image {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 390px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.segmented .ant-segmented {
|
||||||
|
background-color: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-switch-container {
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 350px;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiImageApi } from '#/api/ai/image';
|
||||||
|
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
import { Input, Pagination } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getImagePageMy } from '#/api/ai/image';
|
||||||
|
// TODO @fan:加个 loading 加载中的状态
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<AiImageApi.ImageVO[]>([]); // 列表的数据
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
publicStatus: true,
|
||||||
|
prompt: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
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);
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<div class="square-container">
|
||||||
|
<!-- TODO @fan:style 建议换成 unocss -->
|
||||||
|
<!-- TODO @fan:Search 可以换成 Icon 组件么? -->
|
||||||
|
<Input.Search
|
||||||
|
v-model="queryParams.prompt"
|
||||||
|
style="width: 100%; margin-bottom: 20px"
|
||||||
|
size="large"
|
||||||
|
placeholder="请输入要搜索的内容"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
<div class="gallery">
|
||||||
|
<!-- TODO @fan:这个图片的风格,要不和 ImageCard.vue 界面一致?(只有卡片,没有操作);因为看着更有相框的感觉~~~ -->
|
||||||
|
<div v-for="item in list" :key="item.id" class="gallery-item">
|
||||||
|
<img :src="item.picUrl" class="img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- TODO @fan:缺少翻页 -->
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
:show-total="(total: number) => `共 ${total} 条`"
|
||||||
|
show-quick-jumper
|
||||||
|
show-size-changer
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
@change="debounceGetList"
|
||||||
|
@show-size-change="debounceGetList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.square-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
//max-width: 1000px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgb(0 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #f0f0f0;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover img {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,6 +5,7 @@ import { ref, unref } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import List from './list/index.vue';
|
||||||
import Mode from './mode/index.vue';
|
import Mode from './mode/index.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'Index' });
|
defineOptions({ name: 'Index' });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Nullable } from '@vben/types';
|
||||||
|
|
||||||
|
import { inject, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Image, Slider } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { formatPast } from '#/utils/formatTime';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Index' });
|
||||||
|
|
||||||
|
const currentSong = inject('currentSong', {});
|
||||||
|
|
||||||
|
const audioRef = ref<Nullable<HTMLElement>>(null);
|
||||||
|
// 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||||
|
const audioProps = reactive<any>({
|
||||||
|
autoplay: true,
|
||||||
|
paused: false,
|
||||||
|
currentTime: '00:00',
|
||||||
|
duration: '00:00',
|
||||||
|
muted: false,
|
||||||
|
volume: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleStatus(type: string) {
|
||||||
|
audioProps[type] = !audioProps[type];
|
||||||
|
if (type === 'paused' && audioRef.value) {
|
||||||
|
if (audioProps[type]) {
|
||||||
|
audioRef.value.pause();
|
||||||
|
} else {
|
||||||
|
audioRef.value.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新播放位置
|
||||||
|
function audioTimeUpdate(args: any) {
|
||||||
|
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="b-solid b-1 b-l-none flex h-[72px] items-center justify-between px-2"
|
||||||
|
style="background-color: #fffffd; border-color: #dcdfe6"
|
||||||
|
>
|
||||||
|
<!-- 歌曲信息 -->
|
||||||
|
<div class="flex gap-[10px]">
|
||||||
|
<Image
|
||||||
|
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
|
||||||
|
:width="45"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div>{{ currentSong.name }}</div>
|
||||||
|
<div class="text-[12px] text-gray-400">{{ currentSong.singer }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 音频controls -->
|
||||||
|
<div class="flex items-center gap-[12px]">
|
||||||
|
<IconifyIcon
|
||||||
|
icon="majesticons:back-circle"
|
||||||
|
:size="20"
|
||||||
|
class="cursor-pointer text-gray-300"
|
||||||
|
/>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="
|
||||||
|
audioProps.paused
|
||||||
|
? 'mdi:arrow-right-drop-circle'
|
||||||
|
: 'solar:pause-circle-bold'
|
||||||
|
"
|
||||||
|
:size="30"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="toggleStatus('paused')"
|
||||||
|
/>
|
||||||
|
<IconifyIcon
|
||||||
|
icon="majesticons:next-circle"
|
||||||
|
:size="20"
|
||||||
|
class="cursor-pointer text-gray-300"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-[16px]">
|
||||||
|
<span>{{ audioProps.currentTime }}</span>
|
||||||
|
<Slider
|
||||||
|
v-model:value="audioProps.duration"
|
||||||
|
color="#409eff"
|
||||||
|
class="w-[160px!important]"
|
||||||
|
/>
|
||||||
|
<span>{{ audioProps.duration }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 音频 -->
|
||||||
|
<audio
|
||||||
|
v-bind="audioProps"
|
||||||
|
ref="audioRef"
|
||||||
|
controls
|
||||||
|
v-show="!audioProps"
|
||||||
|
@timeupdate="audioTimeUpdate"
|
||||||
|
>
|
||||||
|
<!-- <source :src="audioUrl" /> -->
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-[16px]">
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'"
|
||||||
|
:size="20"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="toggleStatus('muted')"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
v-model:value="audioProps.volume"
|
||||||
|
color="#409eff"
|
||||||
|
class="w-[160px!important]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { provide, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Col, Empty, Row, TabPane, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import audioBar from './audioBar/index.vue';
|
||||||
|
import songCard from './songCard/index.vue';
|
||||||
|
import songInfo from './songInfo/index.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Index' });
|
||||||
|
|
||||||
|
const currentType = ref('mine');
|
||||||
|
// loading 状态
|
||||||
|
const loading = ref(false);
|
||||||
|
// 当前音乐
|
||||||
|
const currentSong = ref({});
|
||||||
|
|
||||||
|
const mySongList = ref<Recordable<any>[]>([]);
|
||||||
|
const squareSongList = ref<Recordable<any>[]>([]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
*@Description: 调接口生成音乐列表
|
||||||
|
*@MethodAuthor: xiaohong
|
||||||
|
*@Date: 2024-06-27 17:06:44
|
||||||
|
*/
|
||||||
|
function generateMusic(formData: Recordable<any>) {
|
||||||
|
loading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
mySongList.value = Array.from({ length: 20 }, (_, index) => {
|
||||||
|
return {
|
||||||
|
id: index,
|
||||||
|
audioUrl: '',
|
||||||
|
videoUrl: '',
|
||||||
|
title: `我走后${index}`,
|
||||||
|
imageUrl:
|
||||||
|
'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
|
||||||
|
desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
|
||||||
|
date: '2024年04月30日 14:02:57',
|
||||||
|
lyric: `<div class="_words_17xen_66"><div>大江东去,浪淘尽,千古风流人物。
|
||||||
|
</div><div>故垒西边,人道是,三国周郎赤壁。
|
||||||
|
</div><div>乱石穿空,惊涛拍岸,卷起千堆雪。
|
||||||
|
</div><div>江山如画,一时多少豪杰。
|
||||||
|
</div><div>
|
||||||
|
</div><div>遥想公瑾当年,小乔初嫁了,雄姿英发。
|
||||||
|
</div><div>羽扇纶巾,谈笑间,樯橹灰飞烟灭。
|
||||||
|
</div><div>故国神游,多情应笑我,早生华发。
|
||||||
|
</div><div>人生如梦,一尊还酹江月。</div></div>`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*@Description: 设置当前播放的音乐
|
||||||
|
*@MethodAuthor: xiaohong
|
||||||
|
*@Date: 2024-07-19 11:22:33
|
||||||
|
*/
|
||||||
|
function setCurrentSong(music: Recordable<any>) {
|
||||||
|
currentSong.value = music;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
generateMusic,
|
||||||
|
});
|
||||||
|
|
||||||
|
provide('currentSong', currentSong);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-auto overflow-hidden">
|
||||||
|
<Tabs
|
||||||
|
v-model:active-key="currentType"
|
||||||
|
class="flex-auto px-[20px]"
|
||||||
|
tab-position="bottom"
|
||||||
|
>
|
||||||
|
<!-- 我的创作 -->
|
||||||
|
<TabPane key="mine" tab="我的创作" v-loading="loading">
|
||||||
|
<Row v-if="mySongList.length > 0" :gutter="12">
|
||||||
|
<Col v-for="song in mySongList" :key="song.id" :span="24">
|
||||||
|
<songCard :song-info="song" @play="setCurrentSong(song)" />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Empty v-else description="暂无音乐" />
|
||||||
|
</TabPane>
|
||||||
|
|
||||||
|
<!-- 试听广场 -->
|
||||||
|
<TabPane key="square" tab="试听广场" v-loading="loading">
|
||||||
|
<Row v-if="squareSongList.length > 0" :gutter="12">
|
||||||
|
<Col v-for="song in squareSongList" :key="song.id" :span="24">
|
||||||
|
<songCard :song-info="song" @play="setCurrentSong(song)" />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Empty v-else description="暂无音乐" />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
<!-- songInfo -->
|
||||||
|
<songInfo class="flex-none" />
|
||||||
|
</div>
|
||||||
|
<audioBar class="flex-none" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.ant-tabs) {
|
||||||
|
.ant-tabs__content {
|
||||||
|
padding: 0 7px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Index' });
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
songInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['play']);
|
||||||
|
|
||||||
|
const currentSong = inject('currentSong', {});
|
||||||
|
|
||||||
|
function playSong() {
|
||||||
|
emits('play');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-1 mb-[12px] flex p-[12px]">
|
||||||
|
<div class="relative" @click="playSong">
|
||||||
|
<Image :src="songInfo.imageUrl" class="w-80px flex-none" />
|
||||||
|
<div
|
||||||
|
class="bg-op-40 absolute left-0 top-0 flex h-full w-full cursor-pointer items-center justify-center bg-black"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="
|
||||||
|
currentSong.id === songInfo.id
|
||||||
|
? 'solar:pause-circle-bold'
|
||||||
|
: 'mdi:arrow-right-drop-circle'
|
||||||
|
"
|
||||||
|
:size="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-[8px]">
|
||||||
|
<div>{{ songInfo.title }}</div>
|
||||||
|
<div class="mt-[8px] line-clamp-2 text-[12px]">
|
||||||
|
{{ songInfo.desc }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Card, Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Index' });
|
||||||
|
|
||||||
|
const currentSong = inject('currentSong', {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="line-height-24px mb-[0!important] w-[300px]">
|
||||||
|
<Image :src="currentSong.imageUrl" style="width: 100%; height: 100%" />
|
||||||
|
|
||||||
|
<div class="">{{ currentSong.title }}</div>
|
||||||
|
<div class="line-clamp-1 text-[12px]">
|
||||||
|
{{ currentSong.desc }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[12px]">
|
||||||
|
{{ currentSong.date }}
|
||||||
|
</div>
|
||||||
|
<Button size="small" shape="round" class="my-[6px]">信息复用</Button>
|
||||||
|
<div class="text-[12px]" v-html="currentSong.lyric"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue