feat(ai): 新增 AI 绘图功能

- 添加 AI 绘图相关的 API 接口和路由
- 实现 AI 绘图页面,支持不同平台的绘图功能
- 添加绘图作品列表和重新生成功能
- 优化绘图页面样式和布局
This commit is contained in:
gjd
2025-06-13 15:27:25 +08:00
parent 4596cd9fa5
commit 33b7a11a4e
24 changed files with 3035 additions and 23 deletions

View File

@@ -30,7 +30,29 @@ export const AiModelTypeEnum = {
EMBEDDING: 5, // 向量
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 图像生成状态的枚举
*/
@@ -55,6 +77,172 @@ export enum AiWriteTypeEnum {
WRITING = 1, // 撰写
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 模块 ==========
// 全局通用状态枚举
export const CommonStatusEnum = {
@@ -142,7 +330,136 @@ export const InfraApiErrorLogProcessStatusEnum = {
DONE: 1, // 已处理
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 模块 ==========
/**
* 支付渠道枚举

View File

@@ -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 分钟
*
@@ -30,3 +106,39 @@ export function formatPast2(ms: number): string {
}
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 '';
}
}

View File

@@ -7,3 +7,4 @@ export * from './formCreate';
export * from './rangePickerProps';
export * from './routerHelper';
export * from './upload';
export * from './utils';

View File

@@ -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);
};