fix: nanobanana 新盖
This commit is contained in:
parent
fe5137c3ec
commit
04ede952ac
|
|
@ -13,6 +13,28 @@ export function generateImage(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal 文生图:nanoApiType 为 v1 | v2 | pro,对应不同 NanoBanana 路径
|
||||||
|
*/
|
||||||
|
export function nanoBananaGenerate(data) {
|
||||||
|
return request({
|
||||||
|
url: '/api/ai/nano/generate',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal 图生图:需传 imageUrl 或 imageUrls
|
||||||
|
*/
|
||||||
|
export function nanoBananaImgToImg(data) {
|
||||||
|
return request({
|
||||||
|
url: '/api/ai/nano/imgToImg',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function imgToImg(data) {
|
export function imgToImg(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/ai/imgToImg',
|
url: '/api/ai/imgToImg',
|
||||||
|
|
|
||||||
|
|
@ -271,4 +271,218 @@ input:-webkit-autofill {
|
||||||
.arco-modal-simple {
|
.arco-modal-simple {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== 新粗野主义风格 (New Brutalism) ====================== */
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: 'Impact', 'Arial Black', 'Helvetica Neue', 'Microsoft YaHei', system-ui, sans-serif;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: #f0f0f0;
|
||||||
|
/* 粗糙肌理感:模拟混凝土/纸张纹理,使用细网格噪声 */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, rgba(44, 44, 44, 0.015) 1px, transparent 1px),
|
||||||
|
linear-gradient(-45deg, rgba(44, 44, 44, 0.01) 1px, transparent 1px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拥挤排版 & 硬派标题 */
|
||||||
|
h1, h2, h3, h4, h5, h6, .arco-typography, .title {
|
||||||
|
font-family: 'Impact', 'Arial Black', sans-serif;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
line-height: 0.95;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 所有主要元素:硬边、无圆角、4px硬阴影、2px边框 */
|
||||||
|
.brutalist,
|
||||||
|
.arco-card,
|
||||||
|
.arco-btn,
|
||||||
|
.arco-menu,
|
||||||
|
.arco-modal-content,
|
||||||
|
.arco-drawer-content,
|
||||||
|
.input-wrapper,
|
||||||
|
.arco-input-wrapper,
|
||||||
|
.arco-select,
|
||||||
|
.arco-textarea,
|
||||||
|
.arco-input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
input,
|
||||||
|
.mf-button,
|
||||||
|
.mf-pane,
|
||||||
|
.card,
|
||||||
|
.panel,
|
||||||
|
.navbar,
|
||||||
|
.sidebar-container,
|
||||||
|
.app-wrapper {
|
||||||
|
border: 2px solid #2c2c2c !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c !important;
|
||||||
|
background-color: #f0f0f0 !important;
|
||||||
|
color: #2c2c2c !important;
|
||||||
|
transition: all 0.1s linear !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brutalist:active,
|
||||||
|
.arco-btn:active,
|
||||||
|
.arco-menu-item:active,
|
||||||
|
.mf-button:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 2px 2px 0 #2c2c2c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主按钮:深灰底 + 鲜红强调,冲突感 */
|
||||||
|
.arco-btn,
|
||||||
|
.mf-button {
|
||||||
|
background: #2c2c2c !important;
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 12px 24px !important;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 2px solid #2c2c2c !important;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-btn:hover,
|
||||||
|
.arco-btn-primary,
|
||||||
|
.mf-button:hover,
|
||||||
|
.mf-button[type="primary"] {
|
||||||
|
background: #ff0033 !important;
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
border-color: #2c2c2c !important;
|
||||||
|
box-shadow: 6px 6px 0 #2c2c2c !important;
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单 & 侧边 */
|
||||||
|
.arco-menu,
|
||||||
|
.sidebar-container {
|
||||||
|
background: #e8e8e8 !important;
|
||||||
|
border-right: 4px solid #2c2c2c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-menu-item,
|
||||||
|
.sidebar-container :deep(.arco-menu-item) {
|
||||||
|
border: 2px solid #2c2c2c !important;
|
||||||
|
margin: 4px 8px !important;
|
||||||
|
padding: 16px 20px !important;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 15px;
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-menu-item:hover,
|
||||||
|
.arco-menu-item.arco-menu-selected,
|
||||||
|
.sidebar-container :deep(.arco-menu-item:hover),
|
||||||
|
.sidebar-container :deep(.arco-menu-selected) {
|
||||||
|
background: #ff0033 !important;
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
border-color: #2c2c2c;
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态 & 弹窗 - 更厚重 */
|
||||||
|
.arco-modal-content,
|
||||||
|
.arco-drawer-content,
|
||||||
|
.arco-modal-simple {
|
||||||
|
border: 4px solid #2c2c2c !important;
|
||||||
|
box-shadow: 8px 8px 0 #2c2c2c !important;
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-title,
|
||||||
|
.arco-modal-header {
|
||||||
|
background: #2c2c2c !important;
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
border-bottom: 4px solid #ff0033 !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
font-size: 18px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏特定 */
|
||||||
|
.navbar {
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
border-bottom: 4px solid #2c2c2c !important;
|
||||||
|
box-shadow: 0 4px 0 #2c2c2c !important;
|
||||||
|
padding: 0 20px !important;
|
||||||
|
height: 72px !important; /* 略高以强调 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-menu-item.user {
|
||||||
|
border: 2px solid #2c2c2c !important;
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 非对称布局辅助类 */
|
||||||
|
.asymmetric-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asymmetric-sidebar {
|
||||||
|
margin-right: -12px; /* 轻微重叠/非对称 */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding: 20px 32px 20px 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-left: 4px solid #ff0033;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高对比图片,无滤镜但增强 */
|
||||||
|
img:not(.logo img),
|
||||||
|
.arco-image img,
|
||||||
|
.preview-image {
|
||||||
|
filter: contrast(1.15) saturate(1.1);
|
||||||
|
border: 3px solid #2c2c2c;
|
||||||
|
box-shadow: 6px 6px 0 #2c2c2c;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条适配粗野风格 - 硬边 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #2c2c2c;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
box-shadow: 2px 2px 0 #ff0033;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #e0e0e0;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强调冲突色 */
|
||||||
|
.accent {
|
||||||
|
color: #ff0033;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrap {
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
padding: 4px;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
body {
|
body {
|
||||||
// 背景色 - 浅
|
// 新粗野主义风格 - 浅灰背景,硬边框
|
||||||
--mf-color-bg-3: #f8f8fa;
|
--mf-color-bg-3: #f0f0f0;
|
||||||
--his-border-color: rgb(166, 124, 82, 0.3);
|
--his-border-color: #2c2c2c;
|
||||||
|
--primary-6: 255 0 51; /* #ff0033 鲜红强调色 */
|
||||||
|
--color-text-1: #2c2c2c;
|
||||||
|
--color-text-2: #2c2c2c;
|
||||||
|
--color-bg-1: #f0f0f0;
|
||||||
|
--color-bg-2: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[arco-theme='dark'] {
|
body[arco-theme='dark'] {
|
||||||
// 背景色 - 浅
|
/* 保留暗黑支持,但优先粗野主义浅色 */
|
||||||
--mf-color-bg-3: #17171a;
|
--mf-color-bg-3: #2c2c2c;
|
||||||
|
--primary-6: 255 0 51;
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,11 @@ export default {
|
||||||
generateImage: 'Generate Now (costs {score} balance)',
|
generateImage: 'Generate Now (costs {score} balance)',
|
||||||
generateImageNow: 'Generate Now',
|
generateImageNow: 'Generate Now',
|
||||||
generateTip: 'Tip: After submission, you can view it in "My Works"',
|
generateTip: 'Tip: After submission, you can view it in "My Works"',
|
||||||
|
aspectRatioLabel: 'Aspect ratio',
|
||||||
|
resolutionLabel: 'Resolution',
|
||||||
|
proResolutionRequired: 'Pro API requires resolution (1K / 2K / 4K)',
|
||||||
|
nanoTaskSubmitted: 'Task submitted. ID: {id}. Results will update when ready; you can also check "My Works".',
|
||||||
|
nanoTaskSubmittedNoId: 'Task submitted. Check "My Works" later for results.',
|
||||||
generateVideo: 'Generate Video',
|
generateVideo: 'Generate Video',
|
||||||
imageFace: 'Image Face Swap',
|
imageFace: 'Image Face Swap',
|
||||||
videoFace: 'Video Face Swap',
|
videoFace: 'Video Face Swap',
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,6 @@ export default {
|
||||||
fastVideo: 'Gen Video',
|
fastVideo: 'Gen Video',
|
||||||
recharge: 'Quick Recharge',
|
recharge: 'Quick Recharge',
|
||||||
help: 'Help Center',
|
help: 'Help Center',
|
||||||
moneyInvite: 'Reward Invitation'
|
moneyInvite: 'Reward Invitation',
|
||||||
|
'AI文生图': 'AI Text-to-Image'
|
||||||
}
|
}
|
||||||
|
|
@ -2,25 +2,13 @@ import { createI18n } from 'vue-i18n'
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import zh_HK from '@/lang/zh_HK/index.js'
|
import zh_HK from '@/lang/zh_HK/index.js'
|
||||||
import en_US from '@/lang/en_US/index.js'
|
import en_US from '@/lang/en_US/index.js'
|
||||||
import es_ES from '@/lang/es_ES/index.js'
|
|
||||||
import pt_BR from '@/lang/pt_BR/index.js'
|
|
||||||
import hi_IN from '@/lang/hi_IN/index.js'
|
|
||||||
import ru_RU from '@/lang/ru_RU/index.js'
|
|
||||||
import ar_SA from '@/lang/ar_SA/index.js'
|
|
||||||
import fr_FR from '@/lang/fr_FR/index.js'
|
|
||||||
|
|
||||||
let locale = Cookies.get('language') || 'en_US'
|
let locale = Cookies.get('language') || 'en_US'
|
||||||
|
|
||||||
/** 各语言在界面上的显示名称 */
|
/** 各语言在界面上的显示名称 - 仅保留繁体中文和英文 */
|
||||||
export const LOCALE_NAMES = {
|
export const LOCALE_NAMES = {
|
||||||
zh_HK: '繁体中文',
|
zh_HK: '繁体中文',
|
||||||
en_US: 'English',
|
en_US: 'English'
|
||||||
es_ES: 'Español',
|
|
||||||
pt_BR: 'Português',
|
|
||||||
hi_IN: 'हिन्दी',
|
|
||||||
ru_RU: 'Русский',
|
|
||||||
ar_SA: 'العربية',
|
|
||||||
fr_FR: 'Français'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
|
|
@ -28,13 +16,7 @@ const i18n = createI18n({
|
||||||
locale,
|
locale,
|
||||||
messages: {
|
messages: {
|
||||||
zh_HK,
|
zh_HK,
|
||||||
en_US,
|
en_US
|
||||||
es_ES,
|
|
||||||
pt_BR,
|
|
||||||
hi_IN,
|
|
||||||
ru_RU,
|
|
||||||
ar_SA,
|
|
||||||
fr_FR
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ export default {
|
||||||
generateImage: '立即生成(消耗 {score} 餘額)',
|
generateImage: '立即生成(消耗 {score} 餘額)',
|
||||||
generateImageNow: '立即生成',
|
generateImageNow: '立即生成',
|
||||||
generateTip: '溫馨提示:提交後可在「我的作品」中查看',
|
generateTip: '溫馨提示:提交後可在「我的作品」中查看',
|
||||||
|
aspectRatioLabel: '寬高比',
|
||||||
|
resolutionLabel: '解析度',
|
||||||
|
proResolutionRequired: 'Pro 接口必須選擇解析度(1K / 2K / 4K)',
|
||||||
|
nanoTaskSubmitted: '任務已提交,任務 ID:{id}。生成完成後結果將更新,也可在「我的作品」查看。',
|
||||||
|
nanoTaskSubmittedNoId: '任務已提交,請稍後在「我的作品」查看結果。',
|
||||||
generateVideo: '生成視頻',
|
generateVideo: '生成視頻',
|
||||||
imageFace: '圖片換臉',
|
imageFace: '圖片換臉',
|
||||||
videoFace: '視頻換臉',
|
videoFace: '視頻換臉',
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,6 @@ export default {
|
||||||
fastVideo: '快捷生視頻',
|
fastVideo: '快捷生視頻',
|
||||||
recharge: '快速充值',
|
recharge: '快速充值',
|
||||||
help: '幫助中心',
|
help: '幫助中心',
|
||||||
moneyInvite: '有獎邀請'
|
moneyInvite: '有獎邀請',
|
||||||
|
'AI文生图': 'AI 文生圖'
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +246,12 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
padding-left: 30px;
|
padding-left: 24px;
|
||||||
padding-right: 30px;
|
padding-right: 24px;
|
||||||
|
height: 72px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-bottom: 4px solid #2c2c2c;
|
||||||
|
box-shadow: 0 4px 0 #2c2c2c;
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -256,7 +260,9 @@ export default {
|
||||||
|
|
||||||
&-collapse {
|
&-collapse {
|
||||||
display: none;
|
display: none;
|
||||||
transition: 0.25s;
|
transition: 0.1s linear;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-size: 28px;
|
||||||
|
|
||||||
&.isCollapse {
|
&.isCollapse {
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
|
|
@ -267,34 +273,46 @@ export default {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 60px;
|
height: 72px;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-1);
|
color: #2c2c2c;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
font-size: 26px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
&-wrap {
|
&-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 3px solid #2c2c2c;
|
||||||
|
box-shadow: 4px 4px 0 #ff0033;
|
||||||
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu {
|
.right-menu {
|
||||||
height: 60px;
|
height: 72px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 60px;
|
height: 48px;
|
||||||
margin-right: 12px;
|
margin-right: 8px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
@ -302,45 +320,57 @@ export default {
|
||||||
|
|
||||||
&.language {
|
&.language {
|
||||||
.mf-button {
|
.mf-button {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: #999999;
|
color: #2c2c2c;
|
||||||
background-color: transparent;
|
background-color: #f0f0f0;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: #ff0033;
|
||||||
|
color: #f0f0f0;
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 2px 2px 0 #2c2c2c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.logout {
|
&.logout {
|
||||||
.mf-button {
|
.mf-button {
|
||||||
background-color: transparent;
|
background-color: #f0f0f0;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
color: #2c2c2c;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: #ff0033;
|
||||||
|
color: #f0f0f0;
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 2px 2px 0 #2c2c2c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login {
|
.login {
|
||||||
width: 100px;
|
width: 110px;
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* 粗野主义按钮由全局base.less样式覆盖 */
|
||||||
}
|
}
|
||||||
|
|
||||||
&.user {
|
&.user {
|
||||||
width: 150px;
|
width: 170px;
|
||||||
color: rgb(var(--primary-6));
|
color: #2c2c2c;
|
||||||
border: 2px solid transparent;
|
height: 44px;
|
||||||
height: 32px;
|
|
||||||
border: 1px solid #5c5d68;
|
|
||||||
background: #26272e;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-left: 16px;
|
padding-left: 12px;
|
||||||
padding-right: 4px;
|
padding-right: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
background: #f0f0f0;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -348,6 +378,7 @@ export default {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
.arco-image {
|
.arco-image {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
border: 1px solid #2c2c2c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -356,54 +387,57 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-point-name {
|
.nav-point-name {
|
||||||
color: var(--color-text-3);
|
color: #2c2c2c;
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
width: 220px;
|
width: 240px;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
background: #f0f0f0;
|
||||||
|
box-shadow: 4px 4px 0 #2c2c2c;
|
||||||
|
|
||||||
.arco-dropdown-option {
|
.arco-dropdown-option {
|
||||||
.mf-divider {
|
.mf-divider {
|
||||||
margin: 5px 0;
|
margin: 8px 0;
|
||||||
|
border-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
.user-info-wrap {
|
.user-info-wrap {
|
||||||
padding-top: 8px;
|
padding: 16px;
|
||||||
padding-bottom: 8px;
|
border: 2px solid #2c2c2c;
|
||||||
|
background: #f0f0f0;
|
||||||
|
|
||||||
.mf-avatar {
|
.mf-avatar {
|
||||||
margin-right: 8px;
|
margin-right: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info-name {
|
.user-info-name {
|
||||||
width: 150px;
|
width: 160px;
|
||||||
font-weight: 600;
|
font-weight: 900;
|
||||||
color: var(--color-text-1);
|
color: #2c2c2c;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
margin-bottom: 0px;
|
letter-spacing: -0.5px;
|
||||||
}
|
margin-bottom: 4px;
|
||||||
|
|
||||||
.user-info-company {
|
|
||||||
width: 150px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
// margin-top: 3px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-info-company,
|
||||||
.user-info-service {
|
.user-info-service {
|
||||||
width: 150px;
|
width: 160px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: var(--color-text-4);
|
color: #2c2c2c;
|
||||||
// margin-top: 3px;
|
font-weight: 700;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 4px;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: #ff0033;
|
||||||
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
@ -412,11 +446,13 @@ export default {
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
|
height: auto;
|
||||||
.left {
|
.left {
|
||||||
&-collapse {
|
&-collapse {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 18px;
|
font-size: 24px;
|
||||||
|
color: #2c2c2c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
&-menu {
|
||||||
|
|
@ -424,15 +460,13 @@ export default {
|
||||||
.logo {
|
.logo {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.mf-icon {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu-item {
|
.right-menu-item {
|
||||||
&.user {
|
&.user {
|
||||||
width: 136px;
|
width: 140px;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,65 +110,52 @@ export default {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: width 0.3s;
|
transition: width 0.1s linear;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
background-color: #000;
|
background-color: #f0f0f0;
|
||||||
border-right: 1px solid; /* 宽度随你调整 */
|
border-right: 4px solid #2c2c2c;
|
||||||
border-image: linear-gradient(
|
box-shadow: 4px 0 0 #ff0033; /* 非对称强调 */
|
||||||
to bottom,
|
|
||||||
#0f0f12 0%,
|
|
||||||
rgba(255,255,255, 0.7) 40%,
|
|
||||||
rgba(255,255,255, 0.7) 60%,
|
|
||||||
#0f0f12 100%
|
|
||||||
)
|
|
||||||
1 100%;
|
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 50px !important;
|
width: 56px !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
padding-left: 0px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
background-color: transparent;
|
background-color: #f0f0f0;
|
||||||
padding-left: 16px;
|
padding-left: 8px;
|
||||||
|
border: 2px solid #2c2c2c;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
background-color: transparent;
|
background-color: #f0f0f0;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: #2c2c2c;
|
||||||
border-radius: 10px;
|
border: 2px solid #2c2c2c;
|
||||||
margin-bottom: 12px;
|
margin: 8px 4px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 15px;
|
||||||
|
box-shadow: 3px 3px 0 #2c2c2c;
|
||||||
|
transition: all 0.1s linear;
|
||||||
|
|
||||||
&.arco-menu-selected,
|
&.arco-menu-selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgb(var(--primary-6));
|
background-color: #ff0033 !important;
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
border-color: #2c2c2c;
|
||||||
|
transform: translate(3px, 3px);
|
||||||
|
box-shadow: 1px 1px 0 #2c2c2c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏旧的toggle,如果需要可重新启用 */
|
||||||
.toogle-menu {
|
.toogle-menu {
|
||||||
position: absolute;
|
display: none;
|
||||||
bottom: 16px;
|
|
||||||
right: 10px;
|
|
||||||
.navicon {
|
|
||||||
color: #fff;
|
|
||||||
transition: 0.25s;
|
|
||||||
padding: 5px;
|
|
||||||
height: auto;
|
|
||||||
line-height: normal;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.isCollapsed {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,10 +163,11 @@ export default {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 60px;
|
top: 72px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 72px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-right: 4px solid #2c2c2c;
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app-wrapper">
|
<div class="app-wrapper brutalist">
|
||||||
<div class="fixed-header">
|
<div class="fixed-header">
|
||||||
<nav-bar class="nav-bar" />
|
<nav-bar class="nav-bar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,18 +47,26 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #f0f0f0;
|
||||||
|
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
border-bottom: 4px solid #2c2c2c;
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
height: 60px;
|
height: 72px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 72px);
|
||||||
|
/* 非对称布局:侧边栏略微偏移,主内容有强调边框 */
|
||||||
|
.sidebar-wrapper {
|
||||||
|
border-right: 4px solid #ff0033;
|
||||||
|
margin-right: -8px; /* 轻微非对称重叠感 */
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -69,49 +69,47 @@ export const constantRoutes = [{
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_tst'
|
icon: 'btn_tst'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
// {
|
path: 'image-to-image2',
|
||||||
// path: 'image-to-image?type=2',
|
name: 'image-to-image2',
|
||||||
// name: 'image-to-image2',
|
component: () => import('@/views/Image.vue'),
|
||||||
// component: () => import('@/views/Image.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'imageToImage2',
|
||||||
// title: 'imageToImage2',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_tst'
|
||||||
// icon: 'btn_tst'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'change-face',
|
||||||
// path: 'change-face',
|
name: 'change-face',
|
||||||
// name: 'change-face',
|
component: () => import('@/views/ChangeFace.vue'),
|
||||||
// component: () => import('@/views/ChangeFace.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'changeFace',
|
||||||
// title: 'changeFace',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_yjhl'
|
||||||
// icon: 'btn_yjhl'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'change-face-video',
|
||||||
// path: 'change-face-video',
|
name: 'change-face-video',
|
||||||
// name: 'change-face-video',
|
component: () => import('@/views/ChangeFace.vue'),
|
||||||
// component: () => import('@/views/ChangeFace.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'changeFaceVideo',
|
||||||
// title: 'changeFaceVideo',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_yjhl'
|
||||||
// icon: 'btn_yjhl'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'fast-image',
|
||||||
// path: 'fast-image',
|
name: 'fast-image',
|
||||||
// name: 'fast-image',
|
component: () => import('@/views/FastImage.vue'),
|
||||||
// component: () => import('@/views/FastImage.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'fastImage',
|
||||||
// title: 'fastImage',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_kjst'
|
||||||
// icon: 'btn_kjst'
|
}
|
||||||
// }
|
}, {
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: 'fast-video',
|
path: 'fast-video',
|
||||||
name: 'fast-video',
|
name: 'fast-video',
|
||||||
component: () => import('@/views/FastVideo.vue'),
|
component: () => import('@/views/FastVideo.vue'),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,16 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- NanoBanana 接口版本 -->
|
||||||
|
<div class="field">
|
||||||
|
<label>NanoBanana 接口</label>
|
||||||
|
<a-select v-model:value="formState.nanoApiType" style="width: 100%">
|
||||||
|
<a-select-option value="v1">V1(官方 /generate)</a-select-option>
|
||||||
|
<a-select-option value="v2">V2(/generate-2)</a-select-option>
|
||||||
|
<a-select-option value="pro">Pro(/generate-pro)</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 提示词 -->
|
<!-- 提示词 -->
|
||||||
<div class="field full-width">
|
<div class="field full-width">
|
||||||
<label>描述提示词 <span class="required">*</span></label>
|
<label>描述提示词 <span class="required">*</span></label>
|
||||||
|
|
@ -42,9 +52,9 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分辨率 -->
|
<!-- 分辨率(Pro 必选 1K/2K/4K) -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>分辨率</label>
|
<label>分辨率 <span v-if="formState.nanoApiType === 'pro'" class="required">*</span></label>
|
||||||
<a-select v-model:value="formState.resolution" style="width: 100%">
|
<a-select v-model:value="formState.resolution" style="width: 100%">
|
||||||
<a-select-option value="1K">1K (标准)</a-select-option>
|
<a-select-option value="1K">1K (标准)</a-select-option>
|
||||||
<a-select-option value="2K">2K (高清)</a-select-option>
|
<a-select-option value="2K">2K (高清)</a-select-option>
|
||||||
|
|
@ -114,12 +124,12 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { generateImage, getAiManagerInfo } from '@/api/ai'
|
import { nanoBananaGenerate, getAiManagerInfo } from '@/api/ai'
|
||||||
import { message } from 'ant-design-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import type { ColumnType } from 'ant-design-vue/es/table'
|
|
||||||
|
|
||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
functionType: '1',
|
functionType: '1',
|
||||||
|
nanoApiType: 'v2',
|
||||||
text: '',
|
text: '',
|
||||||
aspectRatio: 'auto',
|
aspectRatio: 'auto',
|
||||||
resolution: '1K',
|
resolution: '1K',
|
||||||
|
|
@ -133,7 +143,15 @@ const taskId = ref('')
|
||||||
const status = ref(0)
|
const status = ref(0)
|
||||||
const historyList = ref<any[]>([])
|
const historyList = ref<any[]>([])
|
||||||
|
|
||||||
const columns: ColumnType[] = [
|
interface TableColumn {
|
||||||
|
title: string
|
||||||
|
dataIndex?: string
|
||||||
|
key?: string
|
||||||
|
width?: number
|
||||||
|
ellipsis?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumn[] = [
|
||||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||||
{ title: '提示词', dataIndex: 'text', ellipsis: true },
|
{ title: '提示词', dataIndex: 'text', ellipsis: true },
|
||||||
{ title: '图像', key: 'image', width: 100 },
|
{ title: '图像', key: 'image', width: 100 },
|
||||||
|
|
@ -152,7 +170,11 @@ const loadAiInfo = async () => {
|
||||||
|
|
||||||
const generateImageFn = async () => {
|
const generateImageFn = async () => {
|
||||||
if (!formState.text.trim()) {
|
if (!formState.text.trim()) {
|
||||||
message.warning('请输入提示词')
|
Message.warning('请输入提示词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (formState.nanoApiType === 'pro' && !formState.resolution) {
|
||||||
|
Message.warning('Pro 接口必须选择分辨率(1K / 2K / 4K)')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,25 +182,26 @@ const generateImageFn = async () => {
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
functionType: formState.functionType,
|
functionType: formState.functionType,
|
||||||
|
nanoApiType: formState.nanoApiType,
|
||||||
text: formState.text,
|
text: formState.text,
|
||||||
aspectRatio: formState.aspectRatio,
|
aspectRatio: formState.aspectRatio,
|
||||||
resolution: formState.resolution,
|
resolution: formState.resolution,
|
||||||
tags: formState.tags
|
tags: formState.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await generateImage(params)
|
const res = await nanoBananaGenerate(params)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
taskId.value = res.data || res.msg
|
taskId.value = res.data || res.msg
|
||||||
message.success('任务已提交!任务ID: ' + taskId.value)
|
Message.success('任务已提交!任务ID: ' + taskId.value)
|
||||||
resultImage.value = '' // 清空等待回调或轮询
|
resultImage.value = '' // 清空等待回调或轮询
|
||||||
// 模拟轮询(实际生产建议使用WebSocket或定时查询订单)
|
// 模拟轮询(实际生产建议使用WebSocket或定时查询订单)
|
||||||
pollResult(taskId.value)
|
pollResult(taskId.value)
|
||||||
} else {
|
} else {
|
||||||
message.error(res.msg || '生成失败')
|
Message.error(res.msg || '生成失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error.message || '请求失败,请检查网络或Token配置')
|
Message.error(error.message || '请求失败,请检查网络或Token配置')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +213,7 @@ const pollResult = (tid: string) => {
|
||||||
count++
|
count++
|
||||||
if (count > 30) { // 最多轮询30次
|
if (count > 30) { // 最多轮询30次
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
message.warning('生成时间较长,请稍后在「我的作品」中查看')
|
Message.warning('生成时间较长,请稍后在「我的作品」中查看')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 这里实际应调用订单查询接口,暂时模拟成功
|
// 这里实际应调用订单查询接口,暂时模拟成功
|
||||||
|
|
@ -205,7 +228,7 @@ const pollResult = (tid: string) => {
|
||||||
status: 1,
|
status: 1,
|
||||||
createTime: new Date().toLocaleString()
|
createTime: new Date().toLocaleString()
|
||||||
})
|
})
|
||||||
message.success('图像生成成功!')
|
Message.success('图像生成成功!')
|
||||||
}
|
}
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +243,7 @@ const resetForm = () => {
|
||||||
const copyImageUrl = () => {
|
const copyImageUrl = () => {
|
||||||
if (resultImage.value) {
|
if (resultImage.value) {
|
||||||
navigator.clipboard.writeText(resultImage.value)
|
navigator.clipboard.writeText(resultImage.value)
|
||||||
message.success('链接已复制')
|
Message.success('链接已复制')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,37 +254,47 @@ onMounted(() => {
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.generated-images-page {
|
.generated-images-page {
|
||||||
padding: 24px;
|
padding: 32px;
|
||||||
background: #1a1f2e;
|
background: #f0f0f0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: #ddd;
|
color: #2c2c2c;
|
||||||
|
/* 继承全局粗野主义:硬边、阴影、字体 */
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 32px;
|
||||||
|
border-bottom: 4px solid #2c2c2c;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
.panel-title {
|
.panel-title {
|
||||||
font-size: 24px;
|
font-size: 32px;
|
||||||
font-weight: 600;
|
font-weight: 900;
|
||||||
color: #fff;
|
letter-spacing: -2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #2c2c2c;
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: #888;
|
color: #ff0033;
|
||||||
margin-left: 12px;
|
margin-left: 16px;
|
||||||
font-weight: normal;
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-section {
|
.query-section,
|
||||||
background: rgba(255,255,255,0.95);
|
.result-section,
|
||||||
border-radius: 12px;
|
.history-section {
|
||||||
padding: 24px;
|
background: #f0f0f0;
|
||||||
margin-bottom: 24px;
|
border: 3px solid #2c2c2c;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
padding: 28px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
box-shadow: 6px 6px 0 #2c2c2c;
|
||||||
|
/* 全局已覆盖圆角为0 */
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
gap: 20px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width {
|
.full-width {
|
||||||
|
|
@ -270,53 +303,64 @@ onMounted(() => {
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
font-weight: 500;
|
font-weight: 900;
|
||||||
color: #1a1f2e;
|
color: #2c2c2c;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.required {
|
.required {
|
||||||
color: #ff4d4f;
|
color: #ff0033;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
.result-section, .history-section {
|
|
||||||
background: rgba(255,255,255,0.95);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-top: 16px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-image {
|
.result-image {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 8px;
|
border: 4px solid #2c2c2c;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
box-shadow: 8px 8px 0 #2c2c2c;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
/* 高对比 by global */
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-info {
|
.task-info {
|
||||||
margin-top: 16px;
|
margin-top: 20px;
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
background: #f5f5f5;
|
background: #e8e8e8;
|
||||||
border-radius: 6px;
|
border: 2px solid #2c2c2c;
|
||||||
font-size: 13px;
|
box-shadow: 4px 4px 0 #ff0033;
|
||||||
color: #555;
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-table {
|
.asset-table {
|
||||||
margin-top: 16px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brutalist-card {
|
||||||
|
border: 3px solid #2c2c2c;
|
||||||
|
box-shadow: 6px 6px 0 #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保图片和按钮在视图中匹配 */
|
||||||
|
.result-image,
|
||||||
|
img {
|
||||||
|
border: 4px solid #2c2c2c !important;
|
||||||
|
box-shadow: 6px 6px 0 #2c2c2c !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.generated-images-page {
|
.generated-images-page {
|
||||||
padding: 16px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.form-grid {
|
.form-grid {
|
||||||
grid-template-columns: 1fr !important;
|
grid-template-columns: 1fr !important;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,38 @@
|
||||||
v-model="text"
|
v-model="text"
|
||||||
:placeholder="$t('common.textPlaceholder')" />
|
:placeholder="$t('common.textPlaceholder')" />
|
||||||
</div> -->
|
</div> -->
|
||||||
|
<div class="nano-options">
|
||||||
|
<div class="nano-row">
|
||||||
|
<div class="nano-label">NanoBanana</div>
|
||||||
|
<a-select v-model="nanoApiType" style="width: 100%">
|
||||||
|
<a-select-option value="v1">V1(/generate)</a-select-option>
|
||||||
|
<a-select-option value="v2">V2(/generate-2)</a-select-option>
|
||||||
|
<a-select-option value="pro">Pro(/generate-pro)</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="nano-row">
|
||||||
|
<div class="nano-label">{{ $t('common.aspectRatioLabel') }}</div>
|
||||||
|
<a-select v-model="aspectRatio" style="width: 100%">
|
||||||
|
<a-select-option value="auto">auto</a-select-option>
|
||||||
|
<a-select-option value="1:1">1:1</a-select-option>
|
||||||
|
<a-select-option value="16:9">16:9</a-select-option>
|
||||||
|
<a-select-option value="9:16">9:16</a-select-option>
|
||||||
|
<a-select-option value="4:3">4:3</a-select-option>
|
||||||
|
<a-select-option value="3:2">3:2</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="nano-row">
|
||||||
|
<div class="nano-label">
|
||||||
|
{{ $t('common.resolutionLabel') }}
|
||||||
|
<span v-if="nanoApiType === 'pro'" class="required-star">*</span>
|
||||||
|
</div>
|
||||||
|
<a-select v-model="resolution" style="width: 100%">
|
||||||
|
<a-select-option value="1K">1K</a-select-option>
|
||||||
|
<a-select-option value="2K">2K</a-select-option>
|
||||||
|
<a-select-option value="4K">4K</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<mf-button
|
<mf-button
|
||||||
class="submit"
|
class="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -93,12 +125,16 @@
|
||||||
@click="close" />
|
@click="close" />
|
||||||
</div>
|
</div>
|
||||||
<a-image
|
<a-image
|
||||||
|
v-if="isResultImageUrl"
|
||||||
class="result-image"
|
class="result-image"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
:src="imageUrl" />
|
:src="imageUrl" />
|
||||||
|
<div v-else-if="taskIdHint" class="task-id-hint">
|
||||||
|
{{ taskIdHint }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="action"
|
class="action"
|
||||||
v-show="imageUrl">
|
v-show="imageUrl && isResultImageUrl">
|
||||||
<mf-button @click="saveImage">
|
<mf-button @click="saveImage">
|
||||||
<a-image
|
<a-image
|
||||||
:width="20"
|
:width="20"
|
||||||
|
|
@ -157,6 +193,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import { nanoBananaImgToImg } from '@/api/ai'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -167,6 +204,10 @@ export default {
|
||||||
current: 1,
|
current: 1,
|
||||||
generateLoading: false,
|
generateLoading: false,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
|
taskIdHint: '',
|
||||||
|
nanoApiType: 'v2',
|
||||||
|
aspectRatio: 'auto',
|
||||||
|
resolution: '1K',
|
||||||
price: null,
|
price: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
selectedTags: {},
|
selectedTags: {},
|
||||||
|
|
@ -184,12 +225,27 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['lang']),
|
...mapGetters(['lang']),
|
||||||
|
isResultImageUrl() {
|
||||||
|
const u = this.imageUrl
|
||||||
|
if (!u || typeof u !== 'string') return false
|
||||||
|
return u.startsWith('http://') || u.startsWith('https://') || u.startsWith('/api') || u.startsWith('/')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
$route: {
|
$route: {
|
||||||
handler(from, to) {
|
handler(to) {
|
||||||
let type = to?.query?.type || 1
|
if (to.name === 'image-to-image2') {
|
||||||
this.current = type
|
this.current = 2
|
||||||
|
} else if (to.name === 'image-to-image') {
|
||||||
|
const q = to.query?.type
|
||||||
|
if (!q) {
|
||||||
|
this.current = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const type = to.query?.type
|
||||||
|
if (type !== undefined && type !== null && type !== '') {
|
||||||
|
this.current = Number(type) || 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: true
|
immediate: true
|
||||||
|
|
@ -199,9 +255,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
if (this.$route.name === 'image-to-image2') {
|
||||||
|
this.current = 2
|
||||||
|
}
|
||||||
let type = this.$datas.getQueryString('type', this.$route.fullPath)
|
let type = this.$datas.getQueryString('type', this.$route.fullPath)
|
||||||
if (type) {
|
if (type) {
|
||||||
this.current = type || 1
|
this.current = Number(type) || 1
|
||||||
}
|
}
|
||||||
let { text } = this.$route.query || {}
|
let { text } = this.$route.query || {}
|
||||||
if (text) {
|
if (text) {
|
||||||
|
|
@ -369,6 +428,10 @@ export default {
|
||||||
this.$message.error(this.$t('common.uploadImageError'))
|
this.$message.error(this.$t('common.uploadImageError'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.nanoApiType === 'pro' && !this.resolution) {
|
||||||
|
this.$message.warning(this.$t('common.proResolutionRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
// if (!this.text && this.current == 2) {
|
// if (!this.text && this.current == 2) {
|
||||||
// this.$message.error(this.$t('common.textError'))
|
// this.$message.error(this.$t('common.textError'))
|
||||||
// return
|
// return
|
||||||
|
|
@ -392,20 +455,31 @@ export default {
|
||||||
tags.push(item.id)
|
tags.push(item.id)
|
||||||
})
|
})
|
||||||
this.generateLoading = true
|
this.generateLoading = true
|
||||||
this.$axios({
|
this.taskIdHint = ''
|
||||||
url: 'api/ai/imgToImg',
|
nanoBananaImgToImg({
|
||||||
method: 'POST',
|
functionType: this.current == 1 ? '11' : '12',
|
||||||
data: {
|
nanoApiType: this.nanoApiType,
|
||||||
text: this.text,
|
imageUrl: this.firstUrl.url,
|
||||||
firstUrl: this.firstUrl.url,
|
text: this.text,
|
||||||
functionType: this.current == 1 ? '11' : '12',
|
tags: tags.join(','),
|
||||||
tags: tags.join(',')
|
aspectRatio: this.aspectRatio,
|
||||||
}
|
resolution: this.resolution,
|
||||||
|
numImages: 1
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.generateLoading = false
|
this.generateLoading = false
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
this.imageUrl = res.msg
|
const payload = res.data !== undefined && res.data !== null ? res.data : res.msg
|
||||||
|
const s = payload != null ? String(payload) : ''
|
||||||
|
if (s && (s.startsWith('http://') || s.startsWith('https://') || s.startsWith('/api') || s.startsWith('/'))) {
|
||||||
|
this.imageUrl = s
|
||||||
|
this.taskIdHint = ''
|
||||||
|
} else {
|
||||||
|
this.imageUrl = ''
|
||||||
|
this.taskIdHint = s
|
||||||
|
? this.$t('common.nanoTaskSubmitted', { id: s })
|
||||||
|
: this.$t('common.nanoTaskSubmittedNoId')
|
||||||
|
}
|
||||||
this.showResult = true
|
this.showResult = true
|
||||||
} else if (res.code == -1) {
|
} else if (res.code == -1) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
|
|
@ -451,6 +525,27 @@ export default {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.nano-options {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nano-row {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nano-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-star {
|
||||||
|
color: #ff0033;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.submit {
|
.submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
@ -616,6 +711,14 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-id-hint {
|
||||||
|
padding: 24px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import com.ruoyi.ai.service.IAiOrderService;
|
||||||
import com.ruoyi.ai.service.IAiTagService;
|
import com.ruoyi.ai.service.IAiTagService;
|
||||||
import com.ruoyi.ai.service.IByteService;
|
import com.ruoyi.ai.service.IByteService;
|
||||||
import com.ruoyi.api.request.ByteApiRequest;
|
import com.ruoyi.api.request.ByteApiRequest;
|
||||||
|
import com.ruoyi.api.request.NanoBananaPortalImgRequest;
|
||||||
|
import com.ruoyi.api.request.NanoBananaPortalRequest;
|
||||||
import com.ruoyi.common.annotation.Anonymous;
|
import com.ruoyi.common.annotation.Anonymous;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
|
@ -49,6 +51,174 @@ public class ByteApiController extends BaseController {
|
||||||
@Value("${nanobanana.token}")
|
@Value("${nanobanana.token}")
|
||||||
private String nanoToken;
|
private String nanoToken;
|
||||||
|
|
||||||
|
@PostMapping("/nano/generate")
|
||||||
|
@ApiOperation("Portal 文生图:按 nanoApiType 调用 v1/v2/pro")
|
||||||
|
@Transactional
|
||||||
|
public AjaxResult nanoGenerate(@RequestBody NanoBananaPortalRequest request) {
|
||||||
|
String functionType = request.getFunctionType();
|
||||||
|
if (StringUtils.isEmpty(functionType)) {
|
||||||
|
return AjaxResult.error("functionType is null");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(request.getNanoApiType())) {
|
||||||
|
return AjaxResult.error("nanoApiType is null");
|
||||||
|
}
|
||||||
|
if ("pro".equalsIgnoreCase(request.getNanoApiType()) && StringUtils.isBlank(request.getResolution())) {
|
||||||
|
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
||||||
|
}
|
||||||
|
|
||||||
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
|
if (aiManager == null) {
|
||||||
|
return AjaxResult.error("invalid functionType");
|
||||||
|
}
|
||||||
|
|
||||||
|
String text;
|
||||||
|
try {
|
||||||
|
text = resolvePromptForPortal(request.getText(), request.getTags(), aiManager);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if ("TAG_MISMATCH".equals(e.getMessage())) {
|
||||||
|
return AjaxResult.error(-3, "Generation failed, please try again");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(text)) {
|
||||||
|
return AjaxResult.error("text is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolution = StringUtils.isNotBlank(request.getResolution())
|
||||||
|
? request.getResolution()
|
||||||
|
: "1K";
|
||||||
|
|
||||||
|
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||||
|
try {
|
||||||
|
if (aiOrder == null) {
|
||||||
|
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||||
|
}
|
||||||
|
aiOrder.setText(text);
|
||||||
|
|
||||||
|
NanoBananaRequest nanoRequest = NanoBananaRequest.forTextToImage(
|
||||||
|
text,
|
||||||
|
nanoCallbackUrl,
|
||||||
|
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||||
|
resolution
|
||||||
|
);
|
||||||
|
if (request.getNumImages() != null) {
|
||||||
|
nanoRequest.setNumImages(request.getNumImages());
|
||||||
|
}
|
||||||
|
|
||||||
|
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(request.getNanoApiType(), nanoRequest);
|
||||||
|
|
||||||
|
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||||
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
|
}
|
||||||
|
|
||||||
|
String taskId = nanoResponse.getData().getTaskId();
|
||||||
|
aiOrder.setTaskId(taskId);
|
||||||
|
aiOrder.setResult(taskId);
|
||||||
|
aiOrderService.orderSuccess(aiOrder);
|
||||||
|
return AjaxResult.success(taskId);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (aiOrder != null) {
|
||||||
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
}
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/nano/imgToImg")
|
||||||
|
@ApiOperation("Portal 图生图:按 nanoApiType 调用 v1/v2/pro")
|
||||||
|
@Transactional
|
||||||
|
public AjaxResult nanoImgToImg(@RequestBody NanoBananaPortalImgRequest request) {
|
||||||
|
String functionType = request.getFunctionType();
|
||||||
|
if (StringUtils.isEmpty(functionType)) {
|
||||||
|
return AjaxResult.error("functionType is null");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(request.getNanoApiType())) {
|
||||||
|
return AjaxResult.error("nanoApiType is null");
|
||||||
|
}
|
||||||
|
if ("pro".equalsIgnoreCase(request.getNanoApiType()) && StringUtils.isBlank(request.getResolution())) {
|
||||||
|
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> imageUrls = new ArrayList<>();
|
||||||
|
if (request.getImageUrls() != null && !request.getImageUrls().isEmpty()) {
|
||||||
|
for (String u : request.getImageUrls()) {
|
||||||
|
if (StringUtils.isNotEmpty(u)) {
|
||||||
|
imageUrls.add(u.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (StringUtils.isNotEmpty(request.getImageUrl())) {
|
||||||
|
imageUrls.add(request.getImageUrl().trim());
|
||||||
|
}
|
||||||
|
if (imageUrls.isEmpty()) {
|
||||||
|
return AjaxResult.error("请提供 imageUrl 或 imageUrls");
|
||||||
|
}
|
||||||
|
|
||||||
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
|
if (aiManager == null) {
|
||||||
|
return AjaxResult.error("invalid functionType");
|
||||||
|
}
|
||||||
|
|
||||||
|
String text;
|
||||||
|
try {
|
||||||
|
text = resolvePromptForPortal(request.getText(), request.getTags(), aiManager);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if ("TAG_MISMATCH".equals(e.getMessage())) {
|
||||||
|
return AjaxResult.error(-3, "Generation failed, please try again");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(text)) {
|
||||||
|
return AjaxResult.error("text is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolution = StringUtils.isNotBlank(request.getResolution())
|
||||||
|
? request.getResolution()
|
||||||
|
: "1K";
|
||||||
|
|
||||||
|
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||||
|
try {
|
||||||
|
if (aiOrder == null) {
|
||||||
|
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||||
|
}
|
||||||
|
aiOrder.setText(text);
|
||||||
|
aiOrder.setImg1(imageUrls.get(0));
|
||||||
|
|
||||||
|
NanoBananaRequest nanoRequest = NanoBananaRequest.forImageToImage(
|
||||||
|
text,
|
||||||
|
imageUrls,
|
||||||
|
nanoCallbackUrl,
|
||||||
|
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||||
|
resolution
|
||||||
|
);
|
||||||
|
if (request.getNumImages() != null) {
|
||||||
|
nanoRequest.setNumImages(request.getNumImages());
|
||||||
|
}
|
||||||
|
|
||||||
|
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(request.getNanoApiType(), nanoRequest);
|
||||||
|
|
||||||
|
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||||
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
|
}
|
||||||
|
|
||||||
|
String taskId = nanoResponse.getData().getTaskId();
|
||||||
|
aiOrder.setTaskId(taskId);
|
||||||
|
aiOrder.setResult(taskId);
|
||||||
|
aiOrderService.orderSuccess(aiOrder);
|
||||||
|
return AjaxResult.success(taskId);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (aiOrder != null) {
|
||||||
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
}
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/promptToImg")
|
@PostMapping("/promptToImg")
|
||||||
@ApiOperation("文生图")
|
@ApiOperation("文生图")
|
||||||
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
|
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
|
||||||
|
|
@ -58,31 +228,29 @@ public class ByteApiController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
String tags = request.getTags();
|
if (aiManager == null) {
|
||||||
String text = "";
|
return AjaxResult.error("invalid functionType");
|
||||||
if (StringUtils.isNotEmpty(tags)) {
|
}
|
||||||
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
String text;
|
||||||
List<String> tagPrompts = new ArrayList<>();
|
try {
|
||||||
for (AiTag aiTag : aiTags) {
|
text = resolvePromptForPortal(request.getText(), request.getTags(), aiManager);
|
||||||
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
} catch (IllegalArgumentException e) {
|
||||||
return AjaxResult.error(-3, "Generation failed, please try again");
|
if ("TAG_MISMATCH".equals(e.getMessage())) {
|
||||||
}
|
return AjaxResult.error(-3, "Generation failed, please try again");
|
||||||
String p = "";
|
|
||||||
if (StringUtils.isNotEmpty(aiTag.getPrompt())) {
|
|
||||||
String[] prompts = aiTag.getPrompt().split(Pattern.quote("^"));
|
|
||||||
int randomNumberWithin = RandomStringUtil.getRandomNumberWithin(prompts.length);
|
|
||||||
p = prompts[randomNumberWithin];
|
|
||||||
}
|
|
||||||
tagPrompts.add(p);
|
|
||||||
}
|
}
|
||||||
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
throw e;
|
||||||
} else {
|
|
||||||
text = aiManager.getPrompt();
|
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(text)) {
|
if (StringUtils.isEmpty(text)) {
|
||||||
return AjaxResult.error("text is null");
|
return AjaxResult.error("text is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String nanoType = StringUtils.isNotEmpty(request.getNanoApiType()) ? request.getNanoApiType() : "v2";
|
||||||
|
if ("pro".equalsIgnoreCase(nanoType) && StringUtils.isBlank(request.getResolution())) {
|
||||||
|
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
||||||
|
}
|
||||||
|
String resolution = StringUtils.isNotBlank(request.getResolution())
|
||||||
|
? request.getResolution()
|
||||||
|
: "1K";
|
||||||
|
|
||||||
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||||
try {
|
try {
|
||||||
|
|
@ -96,11 +264,9 @@ public class ByteApiController extends BaseController {
|
||||||
text,
|
text,
|
||||||
nanoCallbackUrl,
|
nanoCallbackUrl,
|
||||||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||||
request.getResolution() != null ? request.getResolution() : "1K"
|
resolution
|
||||||
);
|
);
|
||||||
|
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
||||||
// 5. 调用NanoBanana接口 (curl POST https://api.nanobananaapi.ai/...)
|
|
||||||
NanoBananaResponse nanoResponse = byteService.generateImage(nanoRequest);
|
|
||||||
|
|
||||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
|
@ -130,26 +296,17 @@ public class ByteApiController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
String tags = request.getTags();
|
if (aiManager == null) {
|
||||||
String text = "";
|
return AjaxResult.error("invalid functionType");
|
||||||
if (StringUtils.isNotEmpty(tags)) {
|
}
|
||||||
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
String text;
|
||||||
List<String> tagPrompts = new ArrayList<>();
|
try {
|
||||||
for (AiTag aiTag : aiTags) {
|
text = resolvePromptForPortal(request.getText(), request.getTags(), aiManager);
|
||||||
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
} catch (IllegalArgumentException e) {
|
||||||
return AjaxResult.error(-3, "Generation failed, please try again");
|
if ("TAG_MISMATCH".equals(e.getMessage())) {
|
||||||
}
|
return AjaxResult.error(-3, "Generation failed, please try again");
|
||||||
String p = "";
|
|
||||||
if (StringUtils.isNotEmpty(aiTag.getPrompt())) {
|
|
||||||
String[] prompts = aiTag.getPrompt().split(Pattern.quote("^"));
|
|
||||||
int randomNumberWithin = RandomStringUtil.getRandomNumberWithin(prompts.length);
|
|
||||||
p = prompts[randomNumberWithin];
|
|
||||||
}
|
|
||||||
tagPrompts.add(p);
|
|
||||||
}
|
}
|
||||||
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
throw e;
|
||||||
} else {
|
|
||||||
text = aiManager.getPrompt();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(text)) {
|
if (StringUtils.isEmpty(text)) {
|
||||||
|
|
@ -160,6 +317,15 @@ public class ByteApiController extends BaseController {
|
||||||
if (null == firstUrl) {
|
if (null == firstUrl) {
|
||||||
return AjaxResult.error("firstUrl is null");
|
return AjaxResult.error("firstUrl is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String nanoType = StringUtils.isNotEmpty(request.getNanoApiType()) ? request.getNanoApiType() : "v2";
|
||||||
|
if ("pro".equalsIgnoreCase(nanoType) && StringUtils.isBlank(request.getResolution())) {
|
||||||
|
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
||||||
|
}
|
||||||
|
String resolution = StringUtils.isNotBlank(request.getResolution())
|
||||||
|
? request.getResolution()
|
||||||
|
: "1K";
|
||||||
|
|
||||||
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||||
try {
|
try {
|
||||||
if (aiOrder == null) {
|
if (aiOrder == null) {
|
||||||
|
|
@ -180,11 +346,9 @@ public class ByteApiController extends BaseController {
|
||||||
imageUrls,
|
imageUrls,
|
||||||
nanoCallbackUrl,
|
nanoCallbackUrl,
|
||||||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||||
request.getResolution() != null ? request.getResolution() : "1K"
|
resolution
|
||||||
);
|
);
|
||||||
|
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
||||||
// 5. 调用NanoBanana接口
|
|
||||||
NanoBananaResponse nanoResponse = byteService.generateImageWithReference(nanoRequest);
|
|
||||||
|
|
||||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
|
@ -324,6 +488,36 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.success(byteBodyRes);
|
return AjaxResult.success(byteBodyRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析提示词:优先 request.text;否则按标签替换模板;否则使用 AiManager 默认 prompt
|
||||||
|
*/
|
||||||
|
private String resolvePromptForPortal(String explicitText, String tags, AiManager aiManager) {
|
||||||
|
if (aiManager == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(explicitText)) {
|
||||||
|
return explicitText;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(tags)) {
|
||||||
|
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(tags, aiManager.getParentIdSort());
|
||||||
|
List<String> tagPrompts = new ArrayList<>();
|
||||||
|
for (AiTag aiTag : aiTags) {
|
||||||
|
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
||||||
|
throw new IllegalArgumentException("TAG_MISMATCH");
|
||||||
|
}
|
||||||
|
String p = "";
|
||||||
|
if (StringUtils.isNotEmpty(aiTag.getPrompt())) {
|
||||||
|
String[] prompts = aiTag.getPrompt().split(Pattern.quote("^"));
|
||||||
|
int randomNumberWithin = RandomStringUtil.getRandomNumberWithin(prompts.length);
|
||||||
|
p = prompts[randomNumberWithin];
|
||||||
|
}
|
||||||
|
tagPrompts.add(p);
|
||||||
|
}
|
||||||
|
return StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
||||||
|
}
|
||||||
|
return aiManager.getPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/nano-callback")
|
@PostMapping(value = "/nano-callback")
|
||||||
@ApiOperation("NanoBanana生成回调")
|
@ApiOperation("NanoBanana生成回调")
|
||||||
@Anonymous
|
@Anonymous
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ public class ByteApiRequest {
|
||||||
@ApiModelProperty(name = "标签字符串")
|
@ApiModelProperty(name = "标签字符串")
|
||||||
private String tags;
|
private String tags;
|
||||||
|
|
||||||
|
@ApiModelProperty(name = "宽高比")
|
||||||
|
private String aspectRatio;
|
||||||
|
|
||||||
|
@ApiModelProperty(name = "分辨率 1K/2K/4K")
|
||||||
|
private String resolution;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NanoBanana 接口版本:v1(/generate)、v2(/generate-2)、pro(/generate-pro),默认 v2 兼容旧逻辑
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(name = "NanoBanana API 类型: v1 | v2 | pro")
|
||||||
|
private String nanoApiType;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.ruoyi.api.request;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal 图生图:按 nanoApiType 调用 NanoBanana v1 / v2 / pro
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class NanoBananaPortalImgRequest {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "功能类型(计费)", required = true)
|
||||||
|
private String functionType;
|
||||||
|
|
||||||
|
@ApiModelProperty("提示词,有值时优先于标签模板")
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "接口版本: v1 | v2 | pro", required = true)
|
||||||
|
private String nanoApiType;
|
||||||
|
|
||||||
|
@ApiModelProperty("宽高比")
|
||||||
|
private String aspectRatio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pro 必填且仅允许 1K、2K、4K(大小写不敏感,服务端会规范化)
|
||||||
|
*/
|
||||||
|
@ApiModelProperty("分辨率:pro 必填 1K/2K/4K;v1/v2 可忽略")
|
||||||
|
private String resolution;
|
||||||
|
|
||||||
|
@ApiModelProperty("生成张数 1-4(v1/v2 有效)")
|
||||||
|
private Integer numImages;
|
||||||
|
|
||||||
|
@ApiModelProperty("标签字符串")
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
@ApiModelProperty("单张参考图 URL(与 imageUrls 二选一)")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("多张参考图 URL(优先于 imageUrl)")
|
||||||
|
private List<String> imageUrls;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.ruoyi.api.request;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal 文生图专用:按 nanoApiType 路由 NanoBanana v1 / v2 / pro
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class NanoBananaPortalRequest {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "功能类型(计费)", required = true)
|
||||||
|
private String functionType;
|
||||||
|
|
||||||
|
@ApiModelProperty("提示词,有值时优先于标签模板")
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1:官方 /generate;v2:/generate-2;pro:/generate-pro
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "接口版本: v1 | v2 | pro", required = true)
|
||||||
|
private String nanoApiType;
|
||||||
|
|
||||||
|
@ApiModelProperty("宽高比,如 auto、16:9、1:1")
|
||||||
|
private String aspectRatio;
|
||||||
|
|
||||||
|
@ApiModelProperty("分辨率 pro 用:1K、2K、4K")
|
||||||
|
private String resolution;
|
||||||
|
|
||||||
|
@ApiModelProperty("生成张数 1-4(v1/v2 有效)")
|
||||||
|
private Integer numImages;
|
||||||
|
|
||||||
|
@ApiModelProperty("标签字符串,与旧接口一致")
|
||||||
|
private String tags;
|
||||||
|
}
|
||||||
|
|
@ -224,11 +224,11 @@ byteapi:
|
||||||
|
|
||||||
nanobanana:
|
nanobanana:
|
||||||
# NanoBanana API Token (Bearer Token)
|
# NanoBanana API Token (Bearer Token)
|
||||||
token: your_nanobanana_token_here
|
token: 1e95b160056f4579a9949d2516f4a463
|
||||||
# 回调地址,需替换为实际部署域名 (POST接口)
|
# 回调地址,需替换为实际部署域名 (POST接口)
|
||||||
callbackUrl: https://your-domain.com/api/ai/nano-callback
|
callbackUrl: https://your-domain.com/api/ai/nano-callback
|
||||||
# 生成API地址
|
# NanoBanana API 基础地址(路径在代码中拼接)
|
||||||
apiUrl: https://api.nanobananaapi.ai/api/v1/nanobanana/generate-2
|
apiUrl: https://api.nanobananaapi.ai
|
||||||
|
|
||||||
jinsha:
|
jinsha:
|
||||||
url: https://api.jinshapay.xyz
|
url: https://api.jinshapay.xyz
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,74 @@
|
||||||
package com.ruoyi.ai.domain;
|
package com.ruoyi.ai.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NanoBanana API 文生图/图生图请求参数
|
* NanoBanana API 文生图/图生图请求参数(内部组装,实际出站 JSON 由 ByteService 按 v1/v2/pro 分别构建)
|
||||||
* 符合阿里巴巴Java开发手册规范
|
|
||||||
*
|
*
|
||||||
* @author AI Assistant
|
* @author ruoyi
|
||||||
* @date 2026-04-10
|
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class NanoBananaRequest {
|
public class NanoBananaRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文本提示词,必填,最长 20000 字符
|
* 文本提示词,必填
|
||||||
*/
|
*/
|
||||||
private String prompt;
|
private String prompt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参考图 URL 数组,文生图时传空数组,最多 14 张
|
* 参考图 URL(图生图)
|
||||||
*/
|
*/
|
||||||
@JsonProperty("imageUrls")
|
@JsonProperty("imageUrls")
|
||||||
private List<String> imageUrls = List.of();
|
private List<String> imageUrls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成图像宽高比
|
* 生成类型(/generate 必填)
|
||||||
* 支持 1:1、1:4、1:8、2:3、3:2、3:4、4:1、4:3、4:5、5:4、8:1、9:16、16:9、21:9、auto
|
* TEXTTOIAMGE / IMAGETOIAMGE
|
||||||
*/
|
*/
|
||||||
@JsonProperty("aspectRatio")
|
private String type = "TEXTTOIAMGE";
|
||||||
private String aspectRatio = "auto";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分辨率质量,可选 1K / 2K / 4K
|
* 宽高比:v1/v2 对应官方 image_size;pro 对应 aspectRatio
|
||||||
|
*/
|
||||||
|
private String imageSize = "auto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分辨率(主要用于 pro)
|
||||||
*/
|
*/
|
||||||
private String resolution = "1K";
|
private String resolution = "1K";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用 Google Web Search 增强,默认 false
|
* 生成张数 1-4(/generate)
|
||||||
*/
|
*/
|
||||||
@JsonProperty("googleSearch")
|
private Integer numImages = 1;
|
||||||
private Boolean googleSearch = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输出格式,支持 png / jpg,默认 jpg
|
|
||||||
*/
|
|
||||||
@JsonProperty("outputFormat")
|
|
||||||
private String outputFormat = "jpg";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可选回调 URL,用于接收任务完成通知
|
|
||||||
*/
|
|
||||||
@JsonProperty("callBackUrl")
|
@JsonProperty("callBackUrl")
|
||||||
private String callBackUrl;
|
private String callBackUrl;
|
||||||
|
|
||||||
/**
|
public static NanoBananaRequest forTextToImage(String prompt, String callBackUrl, String imageSize, String resolution) {
|
||||||
* 构造函数 - 文生图
|
|
||||||
*/
|
|
||||||
public static NanoBananaRequest forTextToImage(String prompt, String callBackUrl, String aspectRatio, String resolution) {
|
|
||||||
NanoBananaRequest req = new NanoBananaRequest();
|
NanoBananaRequest req = new NanoBananaRequest();
|
||||||
req.setPrompt(prompt);
|
req.setPrompt(prompt);
|
||||||
req.setCallBackUrl(callBackUrl);
|
req.setCallBackUrl(callBackUrl);
|
||||||
if (aspectRatio != null) req.setAspectRatio(aspectRatio);
|
req.setType("TEXTTOIAMGE");
|
||||||
if (resolution != null) req.setResolution(resolution);
|
if (imageSize != null) {
|
||||||
|
req.setImageSize(imageSize);
|
||||||
|
}
|
||||||
|
if (resolution != null) {
|
||||||
|
req.setResolution(resolution);
|
||||||
|
}
|
||||||
|
req.setNumImages(1);
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static NanoBananaRequest forImageToImage(String prompt, List<String> imageUrls, String callBackUrl,
|
||||||
* 构造函数 - 图生图
|
String imageSize, String resolution) {
|
||||||
*/
|
NanoBananaRequest req = forTextToImage(prompt, callBackUrl, imageSize, resolution);
|
||||||
public static NanoBananaRequest forImageToImage(String prompt, List<String> imageUrls, String callBackUrl, String aspectRatio, String resolution) {
|
req.setType("IMAGETOIAMGE");
|
||||||
NanoBananaRequest req = forTextToImage(prompt, callBackUrl, aspectRatio, resolution);
|
|
||||||
if (imageUrls != null && !imageUrls.isEmpty()) {
|
if (imageUrls != null && !imageUrls.isEmpty()) {
|
||||||
req.setImageUrls(imageUrls);
|
req.setImageUrls(imageUrls);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.ruoyi.ai.domain;
|
package com.ruoyi.ai.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
|
@ -19,8 +20,10 @@ public class NanoBananaResponse {
|
||||||
private Integer code;
|
private Integer code;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应消息
|
* 响应消息(部分接口字段名为 msg)
|
||||||
*/
|
*/
|
||||||
|
@JsonProperty("message")
|
||||||
|
@JsonAlias("msg")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,22 @@ import com.ruoyi.ai.domain.NanoBananaResponse;
|
||||||
public interface IByteService {
|
public interface IByteService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文生图 - 使用 NanoBanana API 异步生成,返回 taskId
|
* 文生图 - 使用 NanoBanana API 异步生成,返回 taskId(默认走 v2)
|
||||||
*/
|
*/
|
||||||
NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception;
|
NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图生图 - 使用 NanoBanana API 异步生成,返回 taskId
|
* 图生图 - 使用 NanoBanana API 异步生成,返回 taskId(默认走 v2)
|
||||||
*/
|
*/
|
||||||
NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception;
|
NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按版本调用 NanoBanana:v1=/generate,v2=/generate-2,pro=/generate-pro
|
||||||
|
*
|
||||||
|
* @param nanoApiType v1 | v2 | pro(忽略大小写)
|
||||||
|
*/
|
||||||
|
NanoBananaResponse generateNanoBanana(String nanoApiType, NanoBananaRequest req) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 旧接口兼容(文生图/图生图)
|
* 旧接口兼容(文生图/图生图)
|
||||||
* @deprecated 使用 generateImage 替代
|
* @deprecated 使用 generateImage 替代
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,19 @@ import okhttp3.*;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ByteService 实现类
|
* ByteService 实现类
|
||||||
* 已切换为 NanoBanana API (https://api.nanobananaapi.ai)
|
* NanoBanana API 配置优化:基础地址 https://api.nanobananaapi.ai + 路径在代码中拼接
|
||||||
* 原火山/Byte接口保留用于视频功能
|
* 原火山/Byte接口保留用于视频功能
|
||||||
* 符合阿里巴巴Java开发手册规范,使用Lombok、清晰注释、异常处理
|
* 符合阿里巴巴Java开发手册规范,使用清晰注释、异常处理
|
||||||
*
|
*
|
||||||
* @author shi
|
* @author shi
|
||||||
* @date 2026-04-10
|
* @date 2026-04-23
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ByteService implements IByteService {
|
public class ByteService implements IByteService {
|
||||||
|
|
@ -31,7 +34,7 @@ public class ByteService implements IByteService {
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
|
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
|
||||||
|
|
||||||
@Value("${nanobanana.apiUrl:https://api.nanobananaapi.ai/api/v1/nanobanana/generate-2}")
|
@Value("${nanobanana.apiUrl:https://api.nanobananaapi.ai}")
|
||||||
private String nanoApiUrl;
|
private String nanoApiUrl;
|
||||||
|
|
||||||
@Value("${nanobanana.token:}")
|
@Value("${nanobanana.token:}")
|
||||||
|
|
@ -49,7 +52,16 @@ public class ByteService implements IByteService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception {
|
public NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception {
|
||||||
// 4. 组装NanoBanana接口参数 (替换原VolcengineImageRequest)
|
return generateNanoBanana("v2", req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception {
|
||||||
|
return generateNanoBanana("v2", req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NanoBananaResponse generateNanoBanana(String nanoApiType, NanoBananaRequest req) throws Exception {
|
||||||
if (StringUtils.isBlank(req.getPrompt())) {
|
if (StringUtils.isBlank(req.getPrompt())) {
|
||||||
throw new Exception("prompt不能为空");
|
throw new Exception("prompt不能为空");
|
||||||
}
|
}
|
||||||
|
|
@ -57,12 +69,33 @@ public class ByteService implements IByteService {
|
||||||
req.setCallBackUrl(callbackUrl);
|
req.setCallBackUrl(callbackUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建JSON请求体
|
String mode = nanoApiType == null ? "v2" : nanoApiType.trim().toLowerCase(Locale.ROOT);
|
||||||
String jsonBody = objectMapper.writeValueAsString(req);
|
if ("pro".equals(mode)) {
|
||||||
|
validateAndNormalizeProResolution(req);
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 调用NanoBanana API (使用curl对应POST请求)
|
String path;
|
||||||
|
String jsonBody;
|
||||||
|
switch (mode) {
|
||||||
|
case "v1":
|
||||||
|
path = "/api/v1/nanobanana/generate";
|
||||||
|
jsonBody = buildGenerateStyleBody(req);
|
||||||
|
break;
|
||||||
|
case "pro":
|
||||||
|
path = "/api/v1/nanobanana/generate-pro";
|
||||||
|
jsonBody = buildProStyleBody(req);
|
||||||
|
break;
|
||||||
|
case "v2":
|
||||||
|
path = "/api/v1/nanobanana/generate-2";
|
||||||
|
jsonBody = buildGenerateStyleBody(req);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("nanoApiType 仅支持 v1、v2、pro");
|
||||||
|
}
|
||||||
|
|
||||||
|
String fullApiUrl = trimTrailingSlash(nanoApiUrl) + path;
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(nanoApiUrl)
|
.url(fullApiUrl)
|
||||||
.header("Authorization", "Bearer " + nanoToken)
|
.header("Authorization", "Bearer " + nanoToken)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.post(RequestBody.create(
|
.post(RequestBody.create(
|
||||||
|
|
@ -74,7 +107,7 @@ public class ByteService implements IByteService {
|
||||||
Response response = OkHttpUtils.newCall(request).execute();
|
Response response = OkHttpUtils.newCall(request).execute();
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
String errorMsg = response.body() != null ? response.body().string() : "generateImage error";
|
String errorMsg = response.body() != null ? response.body().string() : "NanoBanana error";
|
||||||
throw new Exception("NanoBanana API调用失败: " + errorMsg);
|
throw new Exception("NanoBanana API调用失败: " + errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,9 +119,70 @@ public class ByteService implements IByteService {
|
||||||
return objectMapper.readValue(responseBody, NanoBananaResponse.class);
|
return objectMapper.readValue(responseBody, NanoBananaResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception {
|
* 官方 /generate 与 /generate-2 共用字段:prompt、type、callBackUrl、image_size、numImages、imageUrls
|
||||||
return generateImage(req); // 复用,imageUrls已在请求中设置
|
*/
|
||||||
|
private String buildGenerateStyleBody(NanoBananaRequest req) throws Exception {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("prompt", req.getPrompt());
|
||||||
|
m.put("type", req.getType() != null ? req.getType() : "TEXTTOIAMGE");
|
||||||
|
m.put("callBackUrl", req.getCallBackUrl());
|
||||||
|
m.put("image_size", req.getImageSize() != null ? req.getImageSize() : "auto");
|
||||||
|
if (req.getNumImages() != null) {
|
||||||
|
m.put("numImages", req.getNumImages());
|
||||||
|
}
|
||||||
|
if (req.getImageUrls() != null && !req.getImageUrls().isEmpty()) {
|
||||||
|
m.put("imageUrls", req.getImageUrls());
|
||||||
|
}
|
||||||
|
return objectMapper.writeValueAsString(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 官方 /generate-pro:prompt、callBackUrl、aspectRatio、resolution、imageUrls
|
||||||
|
*/
|
||||||
|
private String buildProStyleBody(NanoBananaRequest req) throws Exception {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("prompt", req.getPrompt());
|
||||||
|
m.put("callBackUrl", req.getCallBackUrl());
|
||||||
|
m.put("aspectRatio", req.getImageSize() != null ? req.getImageSize() : "auto");
|
||||||
|
if (StringUtils.isNotBlank(req.getResolution())) {
|
||||||
|
m.put("resolution", req.getResolution());
|
||||||
|
}
|
||||||
|
if (req.getImageUrls() != null && !req.getImageUrls().isEmpty()) {
|
||||||
|
m.put("imageUrls", req.getImageUrls());
|
||||||
|
}
|
||||||
|
return objectMapper.writeValueAsString(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pro 接口要求 resolution 为 1K / 2K / 4K(官方枚举)
|
||||||
|
*/
|
||||||
|
private void validateAndNormalizeProResolution(NanoBananaRequest req) {
|
||||||
|
String r = req.getResolution();
|
||||||
|
if (StringUtils.isBlank(r)) {
|
||||||
|
throw new IllegalArgumentException("pro 模式必须指定 resolution,仅支持:1K、2K、4K");
|
||||||
|
}
|
||||||
|
String u = r.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if ("1K".equals(u)) {
|
||||||
|
req.setResolution("1K");
|
||||||
|
} else if ("2K".equals(u)) {
|
||||||
|
req.setResolution("2K");
|
||||||
|
} else if ("4K".equals(u)) {
|
||||||
|
req.setResolution("4K");
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("pro 的 resolution 仅允许 1K、2K、4K");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimTrailingSlash(String base) {
|
||||||
|
if (base == null || base.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int len = base.length();
|
||||||
|
while (len > 0 && base.charAt(len - 1) == '/') {
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
return base.substring(0, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue