feat: dmxapi images to image (#6935)

新增改图,合并图
This commit is contained in:
Caelan 2025-06-08 10:54:46 +08:00 committed by GitHub
parent d029bb76c5
commit 387bc064cd
11 changed files with 638 additions and 98 deletions

View File

@ -1703,24 +1703,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'ERNIE-Speed-128K',
group: '免费模型'
},
{
id: 'THUDM/glm-4-9b-chat',
provider: 'dmxapi',
name: 'THUDM/glm-4-9b-chat',
group: '免费模型'
},
{
id: 'glm-4-flash',
provider: 'dmxapi',
name: 'glm-4-flash',
group: '免费模型'
},
{
id: 'hunyuan-lite',
provider: 'dmxapi',
name: 'hunyuan-lite',
group: '免费模型'
},
{
id: 'gpt-4o',
provider: 'dmxapi',

View File

@ -940,7 +940,10 @@
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
},
"text_desc_required": "Please enter image description first",
"image_handle_required": "Please upload an image first.",
"req_error_text": "Operation failed. Please try again. Avoid using 'copyrighted' or 'sensitive' words in your prompt.",
"req_error_token": "Please check the validity of the token",
"req_error_no_balance": "Please check the validity of the token",
"auto_create_paint": "Auto-create image",
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically.",
"select_model": "Select Model",

View File

@ -940,7 +940,10 @@
"rendering_speed": "レンダリング速度",
"translating": "翻訳中...",
"text_desc_required": "画像の説明を先に入力してください",
"image_handle_required": "最初に画像をアップロードしてください。",
"req_error_text": "実行に失敗しました。もう一度お試しください。プロンプトに「著作権用語」や「センシティブな用語」を含めないでください。",
"req_error_token": "トークンの有効性を確認してください",
"req_error_no_balance": "トークンの有効性を確認してください",
"auto_create_paint": "画像を自動作成",
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。",
"select_model": "モデルを選択",

View File

@ -940,7 +940,10 @@
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
"image_handle_required": "Пожалуйста, сначала загрузите изображение.",
"req_error_text": "Операция не удалась, повторите попытку. Пожалуйста, избегайте защищенных авторским правом терминов и конфиденциальных слов в запросах.",
"req_error_token": "Пожалуйста, проверьте действительность токена",
"req_error_no_balance": "Пожалуйста, проверьте действительность токена",
"auto_create_paint": "Автоматическое создание изображения",
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое.",
"select_model": "Выбрать модель",

View File

@ -941,6 +941,9 @@
},
"text_desc_required": "请先输入图片描述",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
"req_error_token": "请检查令牌有效性",
"req_error_no_balance": "请检查令牌有效性",
"image_handle_required": "请先上传图片",
"auto_create_paint": "自动新建图片",
"auto_create_paint_tip": "在图片生成后,会自动新建图片",
"select_model": "选择模型",

View File

@ -940,7 +940,10 @@
},
"rendering_speed": "渲染速度",
"text_desc_required": "請先輸入圖片描述",
"image_handle_required": "請先上傳圖片。",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
"req_error_token": "請檢查令牌的有效性",
"req_error_no_balance": "請檢查令牌的有效性",
"auto_create_paint": "自動新增圖片",
"auto_create_paint_tip": "圖片生成後,會自動新增圖片",
"select_model": "選擇模型",

View File

@ -15,8 +15,8 @@ import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { FileType, PaintingsState } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { DmxapiPainting, PaintingAction } from '@types'
import { Avatar, Button, Input, Radio, Select, Switch, Tooltip } from 'antd'
import { DmxapiPainting } from '@types'
import { Avatar, Button, Input, Radio, Segmented, Select, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import React, { FC } from 'react'
@ -25,14 +25,19 @@ import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { generationModeType } from '../../types'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './components/Artboard'
import ImageUploader from './components/ImageUploader'
import PaintingsList from './components/PaintingsList'
import {
COURSE_URL,
DEFAULT_PAINTING,
IMAGE_EDIT_MODELS,
IMAGE_MERGE_MODELS,
IMAGE_SIZES,
MODEOPTIONS,
STYLE_TYPE_OPTIONS,
TEXT_TO_IMAGES_MODELS
} from './config/DmxapiConfig'
@ -64,11 +69,71 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const navigate = useNavigate()
const location = useLocation()
const getNewPainting = () => {
interface FileMapType {
imageFiles?: FileType[]
paths?: string[]
}
const [fileMap, setFileMap] = useState<FileMapType>({
imageFiles: [],
paths: []
})
const modeOptions = MODEOPTIONS.map((ele) => {
return {
label: t(ele.label),
value: ele.value
}
})
const getModelOptions = (mode: generationModeType) => {
if (mode === generationModeType.EDIT) {
return IMAGE_EDIT_MODELS.map((model) => ({
label: model.name,
value: model.id
}))
}
if (mode === generationModeType.MERGE) {
return IMAGE_MERGE_MODELS.map((model) => ({
label: model.name,
value: model.id
}))
}
// 默认情况或其它模式下的选项
return TEXT_TO_IMAGES_MODELS.map((model) => ({
label: model.name,
value: model.id
}))
}
const [modelOptions, setModelOptions] = useState(() => {
// 根据当前painting的generationMode初始化modelOptions
const currentMode = painting?.generationMode || (MODEOPTIONS[0].value as generationModeType)
return getModelOptions(currentMode)
})
const textareaRef = useRef<any>(null)
// 更新painting状态的辅助函数
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
const updatedPainting = { ...painting, ...updates }
setPainting(updatedPainting)
updatePainting('DMXAPIPaintings', updatedPainting)
}
const getNewPainting = (params?: Partial<DmxapiPainting>) => {
clearImages()
const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value
const modelOptionsList = getModelOptions(generationMode as generationModeType)
return {
...DEFAULT_PAINTING,
id: uuid(),
seed: generateRandomSeed()
seed: generateRandomSeed(),
generationMode,
model: modelOptionsList[0]?.value,
...params
}
}
@ -82,19 +147,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
setPainting(addPainting('DMXAPIPaintings', copyPainting))
}
const modelOptions = TEXT_TO_IMAGES_MODELS.map((model) => ({
label: model.name,
value: model.id
}))
const textareaRef = useRef<any>(null)
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
const updatedPainting = { ...painting, ...updates }
setPainting(updatedPainting)
updatePainting('DMXAPIPaintings', updatedPainting)
}
const onSelectModel = (modelId: string) => {
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === modelId)
if (model) {
@ -135,6 +187,62 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}
const onbeforeunload = (file, index?: number) => {
const path = URL.createObjectURL(file)
// 更新 fileMap
setFileMap((prevFileMap) => {
const currentFiles = prevFileMap.imageFiles || []
const currentPaths = prevFileMap.paths || []
let newFiles: FileType[]
let newPaths: string[]
if (index !== undefined) {
// 替换指定索引的图片
newFiles = [...currentFiles]
newFiles[index] = file as FileType
newPaths = [...currentPaths]
newPaths[index] = path
} else {
// 添加新图片到最后
newFiles = [...currentFiles, file as FileType]
newPaths = [...currentPaths, path]
}
return {
imageFiles: newFiles,
paths: newPaths
}
})
return false // 阻止默认上传行为
}
const onGenerationModeChange = (v: generationModeType) => {
clearImages()
const newModelOptions = getModelOptions(v)
setModelOptions(newModelOptions)
const firstModel = newModelOptions[0]?.value
// 如果有urls创建新的painting
if (Array.isArray(painting.urls) && painting.urls.length > 0) {
const newPainting = getNewPainting({
generationMode: v,
model: firstModel // 使用新模式下的第一个模型
})
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
setPainting(addedPainting)
} else {
// 否则更新当前painting
updatePaintingState({
generationMode: v,
model: firstModel // 使用新模式下的第一个模型
})
}
}
// 检查提供者状态函数
const checkProviderStatus = () => {
if (!dmxapiProvider.enabled) {
@ -152,6 +260,14 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
if (!painting.prompt) {
throw new Error('paintings.text_desc_required')
}
if (
painting.generationMode &&
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) &&
(!fileMap.imageFiles || fileMap.imageFiles.length === 0)
) {
throw new Error('paintings.image_handle_required')
}
}
// 准备V1生成请求函数
@ -162,6 +278,10 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
n: painting.n
}
const headerExpand = {
'Content-Type': 'application/json'
}
if (painting.aspect_ratio) {
params['aspect_ratio'] = painting.aspect_ratio
}
@ -184,21 +304,57 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
return {
body: JSON.stringify(params),
headerExpand: headerExpand,
endpoint: `${dmxapiProvider.apiHost}/v1/images/generations`
}
}
// API请求函数
const callApi = async (requestConfig: { endpoint: string; body: any }, controller: AbortController) => {
const { endpoint, body } = requestConfig
const headers = {}
// 准备V2生成请求函数
const prepareV2GenerateRequest = (prompt: string, painting: DmxapiPainting) => {
const params = {
prompt,
n: painting.n,
model: painting.model
}
// 如果是JSON数据添加Content-Type头
if (typeof body === 'string') {
headers['Content-Type'] = 'application/json'
headers['Authorization'] = `Bearer ${dmxapiProvider.apiKey}`
headers['User-Agent'] = 'DMXAPI/1.0.0 (https://www.dmxapi.com)'
headers['Accept'] = 'application/json'
if (painting.image_size) {
params['size'] = '1024x1024'
}
if (painting.style_type) {
params.prompt = prompt + ',风格:' + painting.style_type
}
const formData = new FormData()
for (const key in params) {
formData.append(key, params[key])
}
if (Array.isArray(fileMap.imageFiles)) {
fileMap.imageFiles.forEach((file) => {
formData.append(`image`, file as unknown as Blob)
})
}
return {
body: formData,
endpoint: `${dmxapiProvider.apiHost}/v1/images/edits`
}
}
// API请求函数
const callApi = async (
requestConfig: { endpoint: string; body: any; headerExpand?: any },
controller: AbortController
) => {
const { endpoint, body, headerExpand } = requestConfig
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${dmxapiProvider.apiKey}`,
'User-Agent': 'DMXAPI/1.0.0 (https://www.dmxapi.com)',
...headerExpand
}
const response = await fetch(endpoint, {
@ -209,10 +365,23 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
})
if (!response.ok) {
if (response.status === 401) {
throw new Error('paintings.req_error_token')
} else if (response.status === 403) {
throw new Error('paintings.req_error_no_balance')
}
throw new Error('操作失败,请稍后重试')
}
const data = await response.json()
if (
painting.generationMode &&
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode)
) {
return data.data.map((item: { b64_json: string }) => 'data:image/png;base64,' + item.b64_json)
}
return data.data.map((item: { url: string }) => item.url)
}
@ -246,9 +415,16 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
// 准备请求配置函数
const prepareRequestConfig = (prompt: string, painting: PaintingAction) => {
const prepareRequestConfig = (prompt: string, painting: DmxapiPainting) => {
// 根据模式和模型版本返回不同的请求配置
return prepareV1GenerateRequest(prompt, painting)
if (
painting.generationMode !== undefined &&
[generationModeType.MERGE, generationModeType.EDIT].includes(painting.generationMode)
) {
return prepareV2GenerateRequest(prompt, painting)
} else {
return prepareV1GenerateRequest(prompt, painting)
}
}
const onGenerate = async () => {
@ -338,7 +514,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
}
const onDeletePainting = (paintingToDelete: DmxapiPainting) => {
const onDeletePainting = async (paintingToDelete: DmxapiPainting) => {
if (paintingToDelete.id === painting.id) {
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
@ -349,17 +525,25 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}
removePainting(mode, paintingToDelete).then(() => {})
// 删除绘画
await removePainting(mode, paintingToDelete)
// 检查是否删除空了
if (!DMXAPIPaintings || DMXAPIPaintings.length === 1) {
// 如果删除后没有绘画了,创建一个新的
const newPainting = getNewPainting()
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
setPainting(addedPainting)
}
}
const onSelectPainting = (newPainting: DmxapiPainting) => {
if (generating) return
clearImages()
setPainting(newPainting)
setCurrentImageIndex(0)
}
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
@ -367,20 +551,97 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}
// 清除图片函数
const clearImages = () => {
setFileMap(() => ({ paths: [], imageFiles: [] }))
}
const handleDeleteImage = (index: number) => {
setFileMap((prevFileMap) => {
const newPaths = [...(prevFileMap.paths || [])]
const newImageFiles = [...(prevFileMap.imageFiles || [])]
// 删除指定索引的图片
newPaths.splice(index, 1)
newImageFiles.splice(index, 1)
return {
paths: newPaths,
imageFiles: newImageFiles
}
})
}
// 定义大图的默认图片
const defaultCoverImage = () => {
if (painting.generationMode === generationModeType.EDIT) {
if (painting?.urls.length === 0 && fileMap.paths && fileMap.paths?.length > 0 && fileMap.paths[0]) {
return (
<EmptyImgBox>
<EmptyImg bgUrl={fileMap.paths[0]}></EmptyImg>
</EmptyImgBox>
)
}
}
if (painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1) {
return null
} else {
return (
<EmptyImgBox>
<EmptyImg></EmptyImg>
</EmptyImgBox>
)
}
}
const defaultLoadText = () => {
if (
painting.generationMode &&
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode)
) {
return (
<LoadTextWrap>
<div>
OpenAI gpt-image-1
<br />
2~5
<br />
DMIAPI后台日志查看
</div>
</LoadTextWrap>
)
}
return null
}
useEffect(() => {
if (!DMXAPIPaintings || DMXAPIPaintings.length === 0) {
const newPainting = getNewPainting()
addPainting('DMXAPIPaintings', newPainting)
setPainting(newPainting)
} else if (painting && !painting.generationMode) {
// 如果当前painting没有generationMode添加默认值
const updatedPainting = { ...painting, generationMode: MODEOPTIONS[0].value }
setPainting(updatedPainting)
updatePainting('DMXAPIPaintings', updatedPainting)
}
return () => {
if (spaceClickTimer.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
clearTimeout(spaceClickTimer.current)
// 确保所有paintings都有generationMode属性
DMXAPIPaintings.forEach((p) => {
if (!p.generationMode) {
const updatedPainting = { ...p, generationMode: MODEOPTIONS[0].value }
updatePainting('DMXAPIPaintings', updatedPainting)
}
})
// 确保modelOptions与当前painting的generationMode保持一致
if (painting?.generationMode) {
setModelOptions(getModelOptions(painting.generationMode as generationModeType))
}
}, [DMXAPIPaintings, DMXAPIPaintings.length, addPainting, mode])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // 空依赖数组,只在组件挂载时执行一次
return (
<Container>
@ -422,40 +683,60 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
</Select.Option>
))}
</Select>
{painting.generationMode &&
[generationModeType.EDIT, generationModeType.MERGE].includes(painting.generationMode) && (
<>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}></SettingTitle>
<ImageUploader
fileMap={fileMap}
maxImages={painting.generationMode === generationModeType.EDIT ? 1 : 3}
onClearImages={clearImages}
onDeleteImage={handleDeleteImage}
onAddImage={onbeforeunload}
mode={painting.generationMode}
/>
</>
)}
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
<Radio.Group
value={painting.image_size}
onChange={(e) => onSelectImageSize(e.target.value)}
style={{ display: 'flex' }}>
{IMAGE_SIZES.map((size) => (
<RadioButton value={size.value} key={size.value}>
<VStack alignItems="center">
<ImageSizeImage src={size.icon} theme={theme} />
<span>{size.label}</span>
</VStack>
</RadioButton>
))}
</Radio.Group>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.seed')}
<Tooltip title={t('paintings.seed_desc_tip')}>
<InfoIcon />
</Tooltip>
</SettingTitle>
<Input
value={painting.seed}
pattern="[0-9]*"
onChange={(e) => onInputSeed(e)}
suffix={
<RedoOutlined
onClick={() => updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })}
style={{ cursor: 'pointer', color: 'var(--color-text-2)' }}
{painting.generationMode === generationModeType.GENERATION && (
<>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
<Radio.Group
value={painting.image_size}
onChange={(e) => onSelectImageSize(e.target.value)}
style={{ display: 'flex' }}>
{IMAGE_SIZES.map((size) => (
<RadioButton value={size.value} key={size.value}>
<VStack alignItems="center">
<ImageSizeImage src={size.icon} theme={theme} />
<span>{size.label}</span>
</VStack>
</RadioButton>
))}
</Radio.Group>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.seed')}
<Tooltip title={t('paintings.seed_desc_tip')}>
<InfoIcon />
</Tooltip>
</SettingTitle>
<Input
value={painting.seed}
pattern="[0-9]*"
onChange={(e) => onInputSeed(e)}
suffix={
<RedoOutlined
onClick={() => updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })}
style={{ cursor: 'pointer', color: 'var(--color-text-2)' }}
/>
}
/>
}
/>
</>
)}
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
<SliderContainer>
@ -482,6 +763,14 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
</HStack>
</LeftContainer>
<MainContainer>
<ModeSegmentedContainer>
<Segmented
shape="round"
value={painting.generationMode}
onChange={onGenerationModeChange}
options={modeOptions}
/>
</ModeSegmentedContainer>
<Artboard
painting={painting}
isLoading={isLoading}
@ -489,13 +778,8 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
imageCover={
painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? null : (
<EmptyImgBox>
<EmptyImg></EmptyImg>
</EmptyImgBox>
)
}
imageCover={defaultCoverImage()}
loadText={defaultLoadText()}
/>
<InputContainer>
<Textarea
@ -684,6 +968,13 @@ const RadioTextItem = styled.div`
}
`
// 添加新的样式组件
const ModeSegmentedContainer = styled.div`
display: flex;
justify-content: center;
padding-top: 24px;
`
const EmptyImgBox = styled.div`
display: flex;
flex: 1;
@ -692,11 +983,25 @@ const EmptyImgBox = styled.div`
align-items: center;
`
const EmptyImg = styled.div`
const EmptyImg = styled.div<{ bgUrl?: string }>`
width: 70vh;
height: 70vh;
background-size: 100% 100%;
background-image: url(${DMXAPIToImg});
background-size: cover;
background-image: ${(props) => (props.bgUrl ? `url(${props.bgUrl})` : `url(${DMXAPIToImg})`)};
`
const LoadTextWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
color: black;
text-shadow:
-1px -1px 0 #ffffff,
1px -1px 0 #ffffff,
-1px 1px 0 #ffffff,
1px 1px 0 #ffffff;
`
export default DmxapiPage

View File

@ -15,6 +15,7 @@ interface ArtboardProps {
onCancel: () => void
retry?: (painting: Painting) => void
imageCover?: React.ReactNode
loadText?: React.ReactNode
}
const Artboard: FC<ArtboardProps> = ({
@ -25,7 +26,8 @@ const Artboard: FC<ArtboardProps> = ({
onNextImage,
onCancel,
retry,
imageCover
imageCover,
loadText
}) => {
const { t } = useTranslation()
@ -82,6 +84,8 @@ const Artboard: FC<ArtboardProps> = ({
</div>
) : imageCover ? (
imageCover
) : loadText && isLoading ? (
''
) : (
<div>{t('paintings.image_placeholder')}</div>
)}
@ -90,6 +94,7 @@ const Artboard: FC<ArtboardProps> = ({
{isLoading && (
<LoadingOverlay>
<Spin size="large" />
{loadText ? loadText : ''}
<CancelButton onClick={onCancel}>{t('common.cancel')}</CancelButton>
</LoadingOverlay>
)}

View File

@ -0,0 +1,201 @@
import { DeleteOutlined } from '@ant-design/icons'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
import { useTheme } from '@renderer/context/ThemeProvider'
import type { FileType } from '@renderer/types'
import { Popconfirm, Upload } from 'antd'
import { Button } from 'antd'
import type { RcFile, UploadProps } from 'antd/es/upload'
import React from 'react'
import styled from 'styled-components'
interface ImageUploaderProps {
fileMap: {
imageFiles?: FileType[]
paths?: string[]
}
maxImages: number
onClearImages: () => void
onDeleteImage: (index: number) => void
onAddImage: (file: File, index?: number) => void
mode: string
}
const ImageUploader: React.FC<ImageUploaderProps> = ({
fileMap,
maxImages,
onClearImages,
onDeleteImage,
onAddImage
}) => {
const { theme } = useTheme()
const handleBeforeUpload = (file: RcFile, index?: number) => {
onAddImage(file, index)
return false // 阻止默认上传行为
}
// 自定义上传请求,不执行任何网络请求
const customRequest: UploadProps['customRequest'] = ({ onSuccess }) => {
if (onSuccess) {
onSuccess('ok' as any)
}
}
return (
<>
<HeaderContainer>
{fileMap.imageFiles && fileMap.imageFiles.length > 0 && (
<Button size="small" onClick={onClearImages}>
</Button>
)}
</HeaderContainer>
<UploadImageList>
{fileMap.paths && fileMap.paths.length > 0 ? (
<>
{fileMap.paths.map((src, index) => (
<UploadImageItem key={index}>
<ImageUploadButton
accept="image/png, image/jpeg"
maxCount={1}
multiple={false}
showUploadList={false}
listType="picture-card"
action=""
customRequest={customRequest}
beforeUpload={(file) => {
handleBeforeUpload(file, index)
}}>
<ImagePreview>
<img src={src} alt={`预览图${index + 1}`} />
</ImagePreview>
</ImageUploadButton>
<Popconfirm
title="确定要删除这张图片吗?"
okText="确定"
cancelText="取消"
onConfirm={() => onDeleteImage(index)}>
<DeleteButton>
<DeleteOutlined />
</DeleteButton>
</Popconfirm>
</UploadImageItem>
))}
</>
) : (
''
)}
{fileMap.imageFiles && fileMap.imageFiles.length < maxImages ? (
<UploadImageItem>
<ImageUploadButton
multiple={false}
accept="image/png, image/jpeg"
maxCount={1}
showUploadList={false}
listType="picture-card"
action=""
customRequest={customRequest}
beforeUpload={(file) => {
handleBeforeUpload(file)
}}>
<ImageSizeImage src={IcImageUp} theme={theme} />
</ImageUploadButton>
</UploadImageItem>
) : (
''
)}
</UploadImageList>
</>
)
}
// 样式组件
const HeaderContainer = styled.div`
display: flex;
align-items: center;
margin-bottom: 10px;
`
const ImageUploadButton = styled(Upload)`
& .ant-upload.ant-upload-select,
.ant-upload-list-item-container {
width: 100% !important;
height: 100% !important;
aspect-ratio: 1 !important;
}
margin-bottom: 5px;
`
const ImagePreview = styled.div`
width: 100%;
height: 100%;
position: relative;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&:hover::after {
content: '点击替换';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
`
const ImageSizeImage = styled.img<{ theme: string }>`
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
margin-top: 8px;
`
const UploadImageList = styled.div`
display: flex;
flex-wrap: wrap;
`
const UploadImageItem = styled.div`
width: 45%;
height: 45%;
margin-bottom: 5px;
margin-right: 5px;
position: relative;
`
const DeleteButton = styled.button`
position: absolute;
top: 5px;
right: 5px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.3s ease;
z-index: 10;
&:hover {
opacity: 1;
}
`
export default ImageUploader

View File

@ -7,6 +7,8 @@ import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg
import { uuid } from '@renderer/utils'
import { DmxapiPainting } from '@types'
import { generationModeType } from '../../../types'
export const STYLE_TYPE_OPTIONS = [
{ label: '吉卜力', value: '吉卜力' },
{ label: '皮克斯', value: '皮克斯' },
@ -45,6 +47,22 @@ export const TEXT_TO_IMAGES_MODELS = [
}
]
export const IMAGE_EDIT_MODELS = [
{
id: 'gpt-image-1',
provider: 'DMXAPI',
name: 'OpenAIgpt-image-1'
}
]
export const IMAGE_MERGE_MODELS = [
{
id: 'gpt-image-1',
provider: 'DMXAPI',
name: 'OpenAIgpt-image-1'
}
]
export const IMAGE_SIZES = [
{
label: '1:1',
@ -91,5 +109,12 @@ export const DEFAULT_PAINTING: DmxapiPainting = {
seed: '',
style_type: '',
model: TEXT_TO_IMAGES_MODELS[0].id,
autoCreate: false
autoCreate: false,
generationMode: generationModeType.GENERATION
}
export const MODEOPTIONS = [
{ label: 'paintings.mode.generate', value: generationModeType.GENERATION },
{ label: '改图', value: generationModeType.EDIT },
{ label: '合并图', value: generationModeType.MERGE }
]

View File

@ -256,6 +256,12 @@ export interface ScalePainting extends PaintingParams {
renderingSpeed?: string
}
export enum generationModeType {
GENERATION = 'generation',
EDIT = 'edit',
MERGE = 'merge'
}
export interface DmxapiPainting extends PaintingParams {
model?: string
prompt?: string
@ -265,6 +271,7 @@ export interface DmxapiPainting extends PaintingParams {
seed?: string
style_type?: string
autoCreate?: boolean
generationMode?: generationModeType
}
export interface TokenFluxPainting extends PaintingParams {