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) {
|
||||
return request({
|
||||
url: '/api/ai/imgToImg',
|
||||
|
|
|
|||
|
|
@ -272,3 +272,217 @@ input:-webkit-autofill {
|
|||
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 {
|
||||
// 背景色 - 浅
|
||||
--mf-color-bg-3: #f8f8fa;
|
||||
--his-border-color: rgb(166, 124, 82, 0.3);
|
||||
// 新粗野主义风格 - 浅灰背景,硬边框
|
||||
--mf-color-bg-3: #f0f0f0;
|
||||
--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'] {
|
||||
// 背景色 - 浅
|
||||
--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)',
|
||||
generateImageNow: 'Generate Now',
|
||||
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',
|
||||
imageFace: 'Image Face Swap',
|
||||
videoFace: 'Video Face Swap',
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ export default {
|
|||
fastVideo: 'Gen Video',
|
||||
recharge: 'Quick Recharge',
|
||||
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 zh_HK from '@/lang/zh_HK/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'
|
||||
|
||||
/** 各语言在界面上的显示名称 */
|
||||
/** 各语言在界面上的显示名称 - 仅保留繁体中文和英文 */
|
||||
export const LOCALE_NAMES = {
|
||||
zh_HK: '繁体中文',
|
||||
en_US: 'English',
|
||||
es_ES: 'Español',
|
||||
pt_BR: 'Português',
|
||||
hi_IN: 'हिन्दी',
|
||||
ru_RU: 'Русский',
|
||||
ar_SA: 'العربية',
|
||||
fr_FR: 'Français'
|
||||
en_US: 'English'
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
|
|
@ -28,13 +16,7 @@ const i18n = createI18n({
|
|||
locale,
|
||||
messages: {
|
||||
zh_HK,
|
||||
en_US,
|
||||
es_ES,
|
||||
pt_BR,
|
||||
hi_IN,
|
||||
ru_RU,
|
||||
ar_SA,
|
||||
fr_FR
|
||||
en_US
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ export default {
|
|||
generateImage: '立即生成(消耗 {score} 餘額)',
|
||||
generateImageNow: '立即生成',
|
||||
generateTip: '溫馨提示:提交後可在「我的作品」中查看',
|
||||
aspectRatioLabel: '寬高比',
|
||||
resolutionLabel: '解析度',
|
||||
proResolutionRequired: 'Pro 接口必須選擇解析度(1K / 2K / 4K)',
|
||||
nanoTaskSubmitted: '任務已提交,任務 ID:{id}。生成完成後結果將更新,也可在「我的作品」查看。',
|
||||
nanoTaskSubmittedNoId: '任務已提交,請稍後在「我的作品」查看結果。',
|
||||
generateVideo: '生成視頻',
|
||||
imageFace: '圖片換臉',
|
||||
videoFace: '視頻換臉',
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ export default {
|
|||
fastVideo: '快捷生視頻',
|
||||
recharge: '快速充值',
|
||||
help: '幫助中心',
|
||||
moneyInvite: '有獎邀請'
|
||||
moneyInvite: '有獎邀請',
|
||||
'AI文生图': 'AI 文生圖'
|
||||
}
|
||||
|
|
@ -246,8 +246,12 @@ export default {
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: content-box;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
height: 72px;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 4px solid #2c2c2c;
|
||||
box-shadow: 0 4px 0 #2c2c2c;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
|
|
@ -256,7 +260,9 @@ export default {
|
|||
|
||||
&-collapse {
|
||||
display: none;
|
||||
transition: 0.25s;
|
||||
transition: 0.1s linear;
|
||||
color: #2c2c2c;
|
||||
font-size: 28px;
|
||||
|
||||
&.isCollapse {
|
||||
transform: rotate(-180deg);
|
||||
|
|
@ -267,34 +273,46 @@ export default {
|
|||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
height: 72px;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-1);
|
||||
color: #2c2c2c;
|
||||
font-weight: 900;
|
||||
letter-spacing: -1px;
|
||||
font-size: 26px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border: 3px solid #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #ff0033;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
height: 60px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
margin-right: 12px;
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
|
@ -302,45 +320,57 @@ export default {
|
|||
|
||||
&.language {
|
||||
.mf-button {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
background-color: transparent;
|
||||
font-size: 15px;
|
||||
color: #2c2c2c;
|
||||
background-color: #f0f0f0;
|
||||
border: 2px solid #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
background-color: #ff0033;
|
||||
color: #f0f0f0;
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 2px 2px 0 #2c2c2c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.logout {
|
||||
.mf-button {
|
||||
background-color: transparent;
|
||||
|
||||
background-color: #f0f0f0;
|
||||
border: 2px solid #2c2c2c;
|
||||
color: #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
background-color: #ff0033;
|
||||
color: #f0f0f0;
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 2px 2px 0 #2c2c2c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
width: 100px;
|
||||
border-radius: 10px;
|
||||
width: 110px;
|
||||
cursor: pointer;
|
||||
/* 粗野主义按钮由全局base.less样式覆盖 */
|
||||
}
|
||||
|
||||
&.user {
|
||||
width: 150px;
|
||||
color: rgb(var(--primary-6));
|
||||
border: 2px solid transparent;
|
||||
height: 32px;
|
||||
border: 1px solid #5c5d68;
|
||||
background: #26272e;
|
||||
border-radius: 16px;
|
||||
width: 170px;
|
||||
color: #2c2c2c;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 4px;
|
||||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
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 {
|
||||
display: flex;
|
||||
|
|
@ -348,6 +378,7 @@ export default {
|
|||
flex-grow: 1;
|
||||
.arco-image {
|
||||
margin-right: 8px;
|
||||
border: 1px solid #2c2c2c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -356,54 +387,57 @@ export default {
|
|||
}
|
||||
|
||||
.nav-point-name {
|
||||
color: var(--color-text-3);
|
||||
color: #2c2c2c;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
width: 220px;
|
||||
width: 240px;
|
||||
margin-left: -8px;
|
||||
border: 2px solid #2c2c2c;
|
||||
background: #f0f0f0;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
|
||||
.arco-dropdown-option {
|
||||
.mf-divider {
|
||||
margin: 5px 0;
|
||||
margin: 8px 0;
|
||||
border-color: #2c2c2c;
|
||||
}
|
||||
.user-info-wrap {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding: 16px;
|
||||
border: 2px solid #2c2c2c;
|
||||
background: #f0f0f0;
|
||||
|
||||
.mf-avatar {
|
||||
margin-right: 8px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid #2c2c2c;
|
||||
}
|
||||
|
||||
.user-info-name {
|
||||
width: 150px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
font-size: 14px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.user-info-company {
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
// margin-top: 3px;
|
||||
margin-bottom: 0px;
|
||||
width: 160px;
|
||||
font-weight: 900;
|
||||
color: #2c2c2c;
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-info-company,
|
||||
.user-info-service {
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-4);
|
||||
// margin-top: 3px;
|
||||
margin-bottom: 0px;
|
||||
width: 160px;
|
||||
font-size: 13px;
|
||||
color: #2c2c2c;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
background-color: #ff0033;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
cursor: text;
|
||||
}
|
||||
|
|
@ -412,11 +446,13 @@ export default {
|
|||
|
||||
@media (max-width: 576px) {
|
||||
.navbar {
|
||||
padding: 0 10px;
|
||||
padding: 0 12px;
|
||||
height: auto;
|
||||
.left {
|
||||
&-collapse {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-size: 24px;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
|
|
@ -424,15 +460,13 @@ export default {
|
|||
.logo {
|
||||
display: none;
|
||||
}
|
||||
.mf-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
&.user {
|
||||
width: 136px;
|
||||
width: 140px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,65 +110,52 @@ export default {
|
|||
.sidebar-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: width 0.3s;
|
||||
transition: width 0.1s linear;
|
||||
z-index: 12;
|
||||
background-color: #000;
|
||||
border-right: 1px solid; /* 宽度随你调整 */
|
||||
border-image: linear-gradient(
|
||||
to bottom,
|
||||
#0f0f12 0%,
|
||||
rgba(255,255,255, 0.7) 40%,
|
||||
rgba(255,255,255, 0.7) 60%,
|
||||
#0f0f12 100%
|
||||
)
|
||||
1 100%;
|
||||
background-color: #f0f0f0;
|
||||
border-right: 4px solid #2c2c2c;
|
||||
box-shadow: 4px 0 0 #ff0033; /* 非对称强调 */
|
||||
|
||||
&.collapsed {
|
||||
width: 50px !important;
|
||||
width: 56px !important;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.arco-menu) {
|
||||
padding-left: 0px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-menu) {
|
||||
background-color: transparent;
|
||||
padding-left: 16px;
|
||||
background-color: #f0f0f0;
|
||||
padding-left: 8px;
|
||||
border: 2px solid #2c2c2c;
|
||||
|
||||
&-item {
|
||||
width: 180px;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 12px;
|
||||
background-color: #f0f0f0;
|
||||
color: #2c2c2c;
|
||||
border: 2px solid #2c2c2c;
|
||||
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,
|
||||
&: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 {
|
||||
position: absolute;
|
||||
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);
|
||||
}
|
||||
}
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,10 +163,11 @@ export default {
|
|||
.sidebar-container {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 60px;
|
||||
top: 72px;
|
||||
z-index: 10;
|
||||
height: calc(100% - 60px);
|
||||
height: calc(100% - 72px);
|
||||
overflow: hidden;
|
||||
border-right: 4px solid #2c2c2c;
|
||||
|
||||
&.collapsed {
|
||||
width: 0px !important;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="app-wrapper">
|
||||
<div class="app-wrapper brutalist">
|
||||
<div class="fixed-header">
|
||||
<nav-bar class="nav-bar" />
|
||||
</div>
|
||||
|
|
@ -47,18 +47,26 @@ export default {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #0f0f12;
|
||||
background: #f0f0f0;
|
||||
|
||||
.fixed-header {
|
||||
z-index: 100;
|
||||
border-bottom: 4px solid #2c2c2c;
|
||||
|
||||
.navbar {
|
||||
height: 60px;
|
||||
height: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
height: calc(100% - 60px);
|
||||
height: calc(100% - 72px);
|
||||
/* 非对称布局:侧边栏略微偏移,主内容有强调边框 */
|
||||
.sidebar-wrapper {
|
||||
border-right: 4px solid #ff0033;
|
||||
margin-right: -8px; /* 轻微非对称重叠感 */
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -69,49 +69,47 @@ export const constantRoutes = [{
|
|||
permission: "pass",
|
||||
icon: 'btn_tst'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: 'image-to-image?type=2',
|
||||
// name: 'image-to-image2',
|
||||
// component: () => import('@/views/Image.vue'),
|
||||
// meta: {
|
||||
// title: 'imageToImage2',
|
||||
// menuItem: true,
|
||||
// permission: "pass",
|
||||
// icon: 'btn_tst'
|
||||
// }
|
||||
// }, {
|
||||
// path: 'change-face',
|
||||
// name: 'change-face',
|
||||
// component: () => import('@/views/ChangeFace.vue'),
|
||||
// meta: {
|
||||
// title: 'changeFace',
|
||||
// menuItem: true,
|
||||
// permission: "pass",
|
||||
// icon: 'btn_yjhl'
|
||||
// }
|
||||
// }, {
|
||||
// path: 'change-face-video',
|
||||
// name: 'change-face-video',
|
||||
// component: () => import('@/views/ChangeFace.vue'),
|
||||
// meta: {
|
||||
// title: 'changeFaceVideo',
|
||||
// menuItem: true,
|
||||
// permission: "pass",
|
||||
// icon: 'btn_yjhl'
|
||||
// }
|
||||
// }, {
|
||||
// path: 'fast-image',
|
||||
// name: 'fast-image',
|
||||
// component: () => import('@/views/FastImage.vue'),
|
||||
// meta: {
|
||||
// title: 'fastImage',
|
||||
// menuItem: true,
|
||||
// permission: "pass",
|
||||
// icon: 'btn_kjst'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
}, {
|
||||
path: 'image-to-image2',
|
||||
name: 'image-to-image2',
|
||||
component: () => import('@/views/Image.vue'),
|
||||
meta: {
|
||||
title: 'imageToImage2',
|
||||
menuItem: true,
|
||||
permission: "pass",
|
||||
icon: 'btn_tst'
|
||||
}
|
||||
}, {
|
||||
path: 'change-face',
|
||||
name: 'change-face',
|
||||
component: () => import('@/views/ChangeFace.vue'),
|
||||
meta: {
|
||||
title: 'changeFace',
|
||||
menuItem: true,
|
||||
permission: "pass",
|
||||
icon: 'btn_yjhl'
|
||||
}
|
||||
}, {
|
||||
path: 'change-face-video',
|
||||
name: 'change-face-video',
|
||||
component: () => import('@/views/ChangeFace.vue'),
|
||||
meta: {
|
||||
title: 'changeFaceVideo',
|
||||
menuItem: true,
|
||||
permission: "pass",
|
||||
icon: 'btn_yjhl'
|
||||
}
|
||||
}, {
|
||||
path: 'fast-image',
|
||||
name: 'fast-image',
|
||||
component: () => import('@/views/FastImage.vue'),
|
||||
meta: {
|
||||
title: 'fastImage',
|
||||
menuItem: true,
|
||||
permission: "pass",
|
||||
icon: 'btn_kjst'
|
||||
}
|
||||
}, {
|
||||
path: 'fast-video',
|
||||
name: 'fast-video',
|
||||
component: () => import('@/views/FastVideo.vue'),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@
|
|||
</a-select>
|
||||
</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">
|
||||
<label>描述提示词 <span class="required">*</span></label>
|
||||
|
|
@ -42,9 +52,9 @@
|
|||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 分辨率 -->
|
||||
<!-- 分辨率(Pro 必选 1K/2K/4K) -->
|
||||
<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-option value="1K">1K (标准)</a-select-option>
|
||||
<a-select-option value="2K">2K (高清)</a-select-option>
|
||||
|
|
@ -114,12 +124,12 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { generateImage, getAiManagerInfo } from '@/api/ai'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { ColumnType } from 'ant-design-vue/es/table'
|
||||
import { nanoBananaGenerate, getAiManagerInfo } from '@/api/ai'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
const formState = reactive({
|
||||
functionType: '1',
|
||||
nanoApiType: 'v2',
|
||||
text: '',
|
||||
aspectRatio: 'auto',
|
||||
resolution: '1K',
|
||||
|
|
@ -133,7 +143,15 @@ const taskId = ref('')
|
|||
const status = ref(0)
|
||||
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: '提示词', dataIndex: 'text', ellipsis: true },
|
||||
{ title: '图像', key: 'image', width: 100 },
|
||||
|
|
@ -152,7 +170,11 @@ const loadAiInfo = async () => {
|
|||
|
||||
const generateImageFn = async () => {
|
||||
if (!formState.text.trim()) {
|
||||
message.warning('请输入提示词')
|
||||
Message.warning('请输入提示词')
|
||||
return
|
||||
}
|
||||
if (formState.nanoApiType === 'pro' && !formState.resolution) {
|
||||
Message.warning('Pro 接口必须选择分辨率(1K / 2K / 4K)')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -160,25 +182,26 @@ const generateImageFn = async () => {
|
|||
try {
|
||||
const params = {
|
||||
functionType: formState.functionType,
|
||||
nanoApiType: formState.nanoApiType,
|
||||
text: formState.text,
|
||||
aspectRatio: formState.aspectRatio,
|
||||
resolution: formState.resolution,
|
||||
tags: formState.tags
|
||||
}
|
||||
|
||||
const res = await generateImage(params)
|
||||
const res = await nanoBananaGenerate(params)
|
||||
|
||||
if (res.code === 200) {
|
||||
taskId.value = res.data || res.msg
|
||||
message.success('任务已提交!任务ID: ' + taskId.value)
|
||||
Message.success('任务已提交!任务ID: ' + taskId.value)
|
||||
resultImage.value = '' // 清空等待回调或轮询
|
||||
// 模拟轮询(实际生产建议使用WebSocket或定时查询订单)
|
||||
pollResult(taskId.value)
|
||||
} else {
|
||||
message.error(res.msg || '生成失败')
|
||||
Message.error(res.msg || '生成失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '请求失败,请检查网络或Token配置')
|
||||
Message.error(error.message || '请求失败,请检查网络或Token配置')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
@ -190,7 +213,7 @@ const pollResult = (tid: string) => {
|
|||
count++
|
||||
if (count > 30) { // 最多轮询30次
|
||||
clearInterval(timer)
|
||||
message.warning('生成时间较长,请稍后在「我的作品」中查看')
|
||||
Message.warning('生成时间较长,请稍后在「我的作品」中查看')
|
||||
return
|
||||
}
|
||||
// 这里实际应调用订单查询接口,暂时模拟成功
|
||||
|
|
@ -205,7 +228,7 @@ const pollResult = (tid: string) => {
|
|||
status: 1,
|
||||
createTime: new Date().toLocaleString()
|
||||
})
|
||||
message.success('图像生成成功!')
|
||||
Message.success('图像生成成功!')
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
|
|
@ -220,7 +243,7 @@ const resetForm = () => {
|
|||
const copyImageUrl = () => {
|
||||
if (resultImage.value) {
|
||||
navigator.clipboard.writeText(resultImage.value)
|
||||
message.success('链接已复制')
|
||||
Message.success('链接已复制')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,37 +254,47 @@ onMounted(() => {
|
|||
|
||||
<style scoped lang="less">
|
||||
.generated-images-page {
|
||||
padding: 24px;
|
||||
background: #1a1f2e;
|
||||
padding: 32px;
|
||||
background: #f0f0f0;
|
||||
min-height: 100vh;
|
||||
color: #ddd;
|
||||
color: #2c2c2c;
|
||||
/* 继承全局粗野主义:硬边、阴影、字体 */
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 4px solid #2c2c2c;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.panel-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -2px;
|
||||
text-transform: uppercase;
|
||||
color: #2c2c2c;
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin-left: 12px;
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: #ff0033;
|
||||
margin-left: 16px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-section {
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
.query-section,
|
||||
.result-section,
|
||||
.history-section {
|
||||
background: #f0f0f0;
|
||||
border: 3px solid #2c2c2c;
|
||||
padding: 28px;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 6px 6px 0 #2c2c2c;
|
||||
/* 全局已覆盖圆角为0 */
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
|
|
@ -270,53 +303,64 @@ onMounted(() => {
|
|||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #1a1f2e;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 900;
|
||||
color: #2c2c2c;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff4d4f;
|
||||
color: #ff0033;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.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);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.result-image {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
border: 4px solid #2c2c2c;
|
||||
box-shadow: 8px 8px 0 #2c2c2c;
|
||||
image-rendering: crisp-edges;
|
||||
/* 高对比 by global */
|
||||
}
|
||||
|
||||
.task-info {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: #e8e8e8;
|
||||
border: 2px solid #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #ff0033;
|
||||
font-size: 14px;
|
||||
color: #2c2c2c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.generated-images-page {
|
||||
padding: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,38 @@
|
|||
v-model="text"
|
||||
:placeholder="$t('common.textPlaceholder')" />
|
||||
</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
|
||||
class="submit"
|
||||
type="primary"
|
||||
|
|
@ -93,12 +125,16 @@
|
|||
@click="close" />
|
||||
</div>
|
||||
<a-image
|
||||
v-if="isResultImageUrl"
|
||||
class="result-image"
|
||||
fit="contain"
|
||||
:src="imageUrl" />
|
||||
<div v-else-if="taskIdHint" class="task-id-hint">
|
||||
{{ taskIdHint }}
|
||||
</div>
|
||||
<div
|
||||
class="action"
|
||||
v-show="imageUrl">
|
||||
v-show="imageUrl && isResultImageUrl">
|
||||
<mf-button @click="saveImage">
|
||||
<a-image
|
||||
:width="20"
|
||||
|
|
@ -157,6 +193,7 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { nanoBananaImgToImg } from '@/api/ai'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -167,6 +204,10 @@ export default {
|
|||
current: 1,
|
||||
generateLoading: false,
|
||||
imageUrl: '',
|
||||
taskIdHint: '',
|
||||
nanoApiType: 'v2',
|
||||
aspectRatio: 'auto',
|
||||
resolution: '1K',
|
||||
price: null,
|
||||
tags: [],
|
||||
selectedTags: {},
|
||||
|
|
@ -184,12 +225,27 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...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: {
|
||||
$route: {
|
||||
handler(from, to) {
|
||||
let type = to?.query?.type || 1
|
||||
this.current = type
|
||||
handler(to) {
|
||||
if (to.name === 'image-to-image2') {
|
||||
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,
|
||||
immediate: true
|
||||
|
|
@ -199,9 +255,12 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.name === 'image-to-image2') {
|
||||
this.current = 2
|
||||
}
|
||||
let type = this.$datas.getQueryString('type', this.$route.fullPath)
|
||||
if (type) {
|
||||
this.current = type || 1
|
||||
this.current = Number(type) || 1
|
||||
}
|
||||
let { text } = this.$route.query || {}
|
||||
if (text) {
|
||||
|
|
@ -369,6 +428,10 @@ export default {
|
|||
this.$message.error(this.$t('common.uploadImageError'))
|
||||
return
|
||||
}
|
||||
if (this.nanoApiType === 'pro' && !this.resolution) {
|
||||
this.$message.warning(this.$t('common.proResolutionRequired'))
|
||||
return
|
||||
}
|
||||
// if (!this.text && this.current == 2) {
|
||||
// this.$message.error(this.$t('common.textError'))
|
||||
// return
|
||||
|
|
@ -392,20 +455,31 @@ export default {
|
|||
tags.push(item.id)
|
||||
})
|
||||
this.generateLoading = true
|
||||
this.$axios({
|
||||
url: 'api/ai/imgToImg',
|
||||
method: 'POST',
|
||||
data: {
|
||||
text: this.text,
|
||||
firstUrl: this.firstUrl.url,
|
||||
functionType: this.current == 1 ? '11' : '12',
|
||||
tags: tags.join(',')
|
||||
}
|
||||
this.taskIdHint = ''
|
||||
nanoBananaImgToImg({
|
||||
functionType: this.current == 1 ? '11' : '12',
|
||||
nanoApiType: this.nanoApiType,
|
||||
imageUrl: this.firstUrl.url,
|
||||
text: this.text,
|
||||
tags: tags.join(','),
|
||||
aspectRatio: this.aspectRatio,
|
||||
resolution: this.resolution,
|
||||
numImages: 1
|
||||
})
|
||||
.then((res) => {
|
||||
this.generateLoading = false
|
||||
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
|
||||
} else if (res.code == -1) {
|
||||
this.$confirm({
|
||||
|
|
@ -451,6 +525,27 @@ export default {
|
|||
overflow-y: auto;
|
||||
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 {
|
||||
width: 100%;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.ruoyi.ai.service.IAiOrderService;
|
|||
import com.ruoyi.ai.service.IAiTagService;
|
||||
import com.ruoyi.ai.service.IByteService;
|
||||
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.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
|
|
@ -49,6 +51,174 @@ public class ByteApiController extends BaseController {
|
|||
@Value("${nanobanana.token}")
|
||||
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")
|
||||
@ApiOperation("文生图")
|
||||
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
|
||||
|
|
@ -58,31 +228,29 @@ public class ByteApiController extends BaseController {
|
|||
}
|
||||
|
||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||
String tags = request.getTags();
|
||||
String text = "";
|
||||
if (StringUtils.isNotEmpty(tags)) {
|
||||
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
||||
List<String> tagPrompts = new ArrayList<>();
|
||||
for (AiTag aiTag : aiTags) {
|
||||
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
||||
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);
|
||||
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");
|
||||
}
|
||||
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
||||
} else {
|
||||
text = aiManager.getPrompt();
|
||||
throw e;
|
||||
}
|
||||
if (StringUtils.isEmpty(text)) {
|
||||
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);
|
||||
try {
|
||||
|
|
@ -96,11 +264,9 @@ public class ByteApiController extends BaseController {
|
|||
text,
|
||||
nanoCallbackUrl,
|
||||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||
request.getResolution() != null ? request.getResolution() : "1K"
|
||||
resolution
|
||||
);
|
||||
|
||||
// 5. 调用NanoBanana接口 (curl POST https://api.nanobananaapi.ai/...)
|
||||
NanoBananaResponse nanoResponse = byteService.generateImage(nanoRequest);
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
||||
|
||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
|
|
@ -130,26 +296,17 @@ public class ByteApiController extends BaseController {
|
|||
}
|
||||
|
||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||
String tags = request.getTags();
|
||||
String text = "";
|
||||
if (StringUtils.isNotEmpty(tags)) {
|
||||
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
||||
List<String> tagPrompts = new ArrayList<>();
|
||||
for (AiTag aiTag : aiTags) {
|
||||
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
||||
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);
|
||||
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");
|
||||
}
|
||||
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
||||
} else {
|
||||
text = aiManager.getPrompt();
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(text)) {
|
||||
|
|
@ -160,6 +317,15 @@ public class ByteApiController extends BaseController {
|
|||
if (null == firstUrl) {
|
||||
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);
|
||||
try {
|
||||
if (aiOrder == null) {
|
||||
|
|
@ -180,11 +346,9 @@ public class ByteApiController extends BaseController {
|
|||
imageUrls,
|
||||
nanoCallbackUrl,
|
||||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||
request.getResolution() != null ? request.getResolution() : "1K"
|
||||
resolution
|
||||
);
|
||||
|
||||
// 5. 调用NanoBanana接口
|
||||
NanoBananaResponse nanoResponse = byteService.generateImageWithReference(nanoRequest);
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
||||
|
||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
|
|
@ -324,6 +488,36 @@ public class ByteApiController extends BaseController {
|
|||
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")
|
||||
@ApiOperation("NanoBanana生成回调")
|
||||
@Anonymous
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ public class ByteApiRequest {
|
|||
@ApiModelProperty(name = "标签字符串")
|
||||
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 API Token (Bearer Token)
|
||||
token: your_nanobanana_token_here
|
||||
token: 1e95b160056f4579a9949d2516f4a463
|
||||
# 回调地址,需替换为实际部署域名 (POST接口)
|
||||
callbackUrl: https://your-domain.com/api/ai/nano-callback
|
||||
# 生成API地址
|
||||
apiUrl: https://api.nanobananaapi.ai/api/v1/nanobanana/generate-2
|
||||
# NanoBanana API 基础地址(路径在代码中拼接)
|
||||
apiUrl: https://api.nanobananaapi.ai
|
||||
|
||||
jinsha:
|
||||
url: https://api.jinshapay.xyz
|
||||
|
|
|
|||
|
|
@ -1,78 +1,74 @@
|
|||
package com.ruoyi.ai.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* NanoBanana API 文生图/图生图请求参数
|
||||
* 符合阿里巴巴Java开发手册规范
|
||||
* NanoBanana API 文生图/图生图请求参数(内部组装,实际出站 JSON 由 ByteService 按 v1/v2/pro 分别构建)
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2026-04-10
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class NanoBananaRequest {
|
||||
|
||||
/**
|
||||
* 文本提示词,必填,最长 20000 字符
|
||||
* 文本提示词,必填
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 参考图 URL 数组,文生图时传空数组,最多 14 张
|
||||
* 参考图 URL(图生图)
|
||||
*/
|
||||
@JsonProperty("imageUrls")
|
||||
private List<String> imageUrls = List.of();
|
||||
private List<String> imageUrls;
|
||||
|
||||
/**
|
||||
* 生成图像宽高比
|
||||
* 支持 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
|
||||
* 生成类型(/generate 必填)
|
||||
* TEXTTOIAMGE / IMAGETOIAMGE
|
||||
*/
|
||||
@JsonProperty("aspectRatio")
|
||||
private String aspectRatio = "auto";
|
||||
private String type = "TEXTTOIAMGE";
|
||||
|
||||
/**
|
||||
* 分辨率质量,可选 1K / 2K / 4K
|
||||
* 宽高比:v1/v2 对应官方 image_size;pro 对应 aspectRatio
|
||||
*/
|
||||
private String imageSize = "auto";
|
||||
|
||||
/**
|
||||
* 分辨率(主要用于 pro)
|
||||
*/
|
||||
private String resolution = "1K";
|
||||
|
||||
/**
|
||||
* 是否启用 Google Web Search 增强,默认 false
|
||||
* 生成张数 1-4(/generate)
|
||||
*/
|
||||
@JsonProperty("googleSearch")
|
||||
private Boolean googleSearch = false;
|
||||
private Integer numImages = 1;
|
||||
|
||||
/**
|
||||
* 输出格式,支持 png / jpg,默认 jpg
|
||||
*/
|
||||
@JsonProperty("outputFormat")
|
||||
private String outputFormat = "jpg";
|
||||
|
||||
/**
|
||||
* 可选回调 URL,用于接收任务完成通知
|
||||
*/
|
||||
@JsonProperty("callBackUrl")
|
||||
private String callBackUrl;
|
||||
|
||||
/**
|
||||
* 构造函数 - 文生图
|
||||
*/
|
||||
public static NanoBananaRequest forTextToImage(String prompt, String callBackUrl, String aspectRatio, String resolution) {
|
||||
public static NanoBananaRequest forTextToImage(String prompt, String callBackUrl, String imageSize, String resolution) {
|
||||
NanoBananaRequest req = new NanoBananaRequest();
|
||||
req.setPrompt(prompt);
|
||||
req.setCallBackUrl(callBackUrl);
|
||||
if (aspectRatio != null) req.setAspectRatio(aspectRatio);
|
||||
if (resolution != null) req.setResolution(resolution);
|
||||
req.setType("TEXTTOIAMGE");
|
||||
if (imageSize != null) {
|
||||
req.setImageSize(imageSize);
|
||||
}
|
||||
if (resolution != null) {
|
||||
req.setResolution(resolution);
|
||||
}
|
||||
req.setNumImages(1);
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数 - 图生图
|
||||
*/
|
||||
public static NanoBananaRequest forImageToImage(String prompt, List<String> imageUrls, String callBackUrl, String aspectRatio, String resolution) {
|
||||
NanoBananaRequest req = forTextToImage(prompt, callBackUrl, aspectRatio, resolution);
|
||||
public static NanoBananaRequest forImageToImage(String prompt, List<String> imageUrls, String callBackUrl,
|
||||
String imageSize, String resolution) {
|
||||
NanoBananaRequest req = forTextToImage(prompt, callBackUrl, imageSize, resolution);
|
||||
req.setType("IMAGETOIAMGE");
|
||||
if (imageUrls != null && !imageUrls.isEmpty()) {
|
||||
req.setImageUrls(imageUrls);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.ai.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
|
|
@ -19,8 +20,10 @@ public class NanoBananaResponse {
|
|||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
* 响应消息(部分接口字段名为 msg)
|
||||
*/
|
||||
@JsonProperty("message")
|
||||
@JsonAlias("msg")
|
||||
private String message;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,15 +16,22 @@ import com.ruoyi.ai.domain.NanoBananaResponse;
|
|||
public interface IByteService {
|
||||
|
||||
/**
|
||||
* 文生图 - 使用 NanoBanana API 异步生成,返回 taskId
|
||||
* 文生图 - 使用 NanoBanana API 异步生成,返回 taskId(默认走 v2)
|
||||
*/
|
||||
NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception;
|
||||
|
||||
/**
|
||||
* 图生图 - 使用 NanoBanana API 异步生成,返回 taskId
|
||||
* 图生图 - 使用 NanoBanana API 异步生成,返回 taskId(默认走 v2)
|
||||
*/
|
||||
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 替代
|
||||
|
|
|
|||
|
|
@ -11,16 +11,19 @@ import okhttp3.*;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ByteService 实现类
|
||||
* 已切换为 NanoBanana API (https://api.nanobananaapi.ai)
|
||||
* NanoBanana API 配置优化:基础地址 https://api.nanobananaapi.ai + 路径在代码中拼接
|
||||
* 原火山/Byte接口保留用于视频功能
|
||||
* 符合阿里巴巴Java开发手册规范,使用Lombok、清晰注释、异常处理
|
||||
* 符合阿里巴巴Java开发手册规范,使用清晰注释、异常处理
|
||||
*
|
||||
* @author shi
|
||||
* @date 2026-04-10
|
||||
* @date 2026-04-23
|
||||
*/
|
||||
@Service
|
||||
public class ByteService implements IByteService {
|
||||
|
|
@ -31,7 +34,7 @@ public class ByteService implements IByteService {
|
|||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.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;
|
||||
|
||||
@Value("${nanobanana.token:}")
|
||||
|
|
@ -49,7 +52,16 @@ public class ByteService implements IByteService {
|
|||
|
||||
@Override
|
||||
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())) {
|
||||
throw new Exception("prompt不能为空");
|
||||
}
|
||||
|
|
@ -57,12 +69,33 @@ public class ByteService implements IByteService {
|
|||
req.setCallBackUrl(callbackUrl);
|
||||
}
|
||||
|
||||
// 构建JSON请求体
|
||||
String jsonBody = objectMapper.writeValueAsString(req);
|
||||
String mode = nanoApiType == null ? "v2" : nanoApiType.trim().toLowerCase(Locale.ROOT);
|
||||
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()
|
||||
.url(nanoApiUrl)
|
||||
.url(fullApiUrl)
|
||||
.header("Authorization", "Bearer " + nanoToken)
|
||||
.header("Content-Type", "application/json")
|
||||
.post(RequestBody.create(
|
||||
|
|
@ -74,7 +107,7 @@ public class ByteService implements IByteService {
|
|||
Response response = OkHttpUtils.newCall(request).execute();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +119,70 @@ public class ByteService implements IByteService {
|
|||
return objectMapper.readValue(responseBody, NanoBananaResponse.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception {
|
||||
return generateImage(req); // 复用,imageUrls已在请求中设置
|
||||
/**
|
||||
* 官方 /generate 与 /generate-2 共用字段:prompt、type、callBackUrl、image_size、numImages、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
|
||||
|
|
|
|||
Loading…
Reference in New Issue