Compare commits
1 Commits
nanobanana
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
e024b0ba51 |
|
|
@ -1,56 +0,0 @@
|
||||||
---
|
|
||||||
description: 若依项目Java代码生成规则 - 阿里巴巴规范、命名、实体类、MyBatis、接口路径
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# 项目背景
|
|
||||||
|
|
||||||
你的web-api是一个基于若依(RuoYi)框架的Spring Boot项目。
|
|
||||||
|
|
||||||
# 代码生成规则
|
|
||||||
|
|
||||||
## 1. 命名规范
|
|
||||||
- 所有Java代码必须符合**阿里巴巴Java开发手册规范**(包括命名、注释、异常处理等)。
|
|
||||||
- 类名、方法名、变量名必须严格使用**驼峰命名法**(CamelCase)。
|
|
||||||
- 类名:UpperCamelCase(如 `TosAssetService`)
|
|
||||||
- 方法名、变量名:lowerCamelCase(如 `getAssetById`、`assetList`)
|
|
||||||
|
|
||||||
## 2. 实体类(Entity/Domain)
|
|
||||||
- 必须使用 **Lombok** 注解简化代码:
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
@TableName("tos_asset") // 或 @Entity
|
|
||||||
public class TosAsset {
|
|
||||||
// 字段...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 所有字段和类**必须添加清晰的中文或英文注释**(推荐使用中文)。
|
|
||||||
- 必须包含 `serialVersionUID`(如果适用)。
|
|
||||||
|
|
||||||
## 3. 数据库操作(MyBatis)
|
|
||||||
- 必须使用 **MyBatis**(推荐 MyBatis-Plus)进行数据库操作。
|
|
||||||
- **禁止**在 `mapper.xml` 中编写过于复杂的SQL语句。
|
|
||||||
- 复杂查询应在 Service 层使用 Java 代码组合,或使用 MyBatis-Plus 的 LambdaQueryWrapper。
|
|
||||||
- Mapper 接口方法名必须语义清晰,并添加注释。
|
|
||||||
|
|
||||||
## 4. Controller 接口路径
|
|
||||||
- 给 **portal-ui**(前端门户)统一以 `/api` 开头,例如:
|
|
||||||
```java
|
|
||||||
@GetMapping("/api/assets/list")
|
|
||||||
```
|
|
||||||
- 给 **admin-ui**(管理后台)使用以 `/` 开头(保持若依原有风格),例如:
|
|
||||||
```java
|
|
||||||
@GetMapping("/assets/list")
|
|
||||||
```
|
|
||||||
- 建议在不同 Controller 中区分(如 `AiPortalController` vs `AiManagerController`)。
|
|
||||||
|
|
||||||
## 5. 其他要求
|
|
||||||
- 所有新增类必须包含完整的 Javadoc 注释。
|
|
||||||
- Service 实现类必须实现对应接口。
|
|
||||||
- 避免直接在 Controller 中处理业务逻辑,保持分层清晰。
|
|
||||||
- 异常处理统一使用 `ServiceException` 或项目自定义异常。
|
|
||||||
- 业务逻辑遵循mvc原则放在service
|
|
||||||
- redis key 统一放在RedisKey类
|
|
||||||
|
|
||||||
|
|
||||||
**始终严格遵守以上规则生成所有Java代码。**
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
---
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
# portal-ui API调用规范
|
|
||||||
|
|
||||||
## 1. 接口路径要求
|
|
||||||
- 所有调用 **portal-ui** 的接口必须使用 `/api` 开头(与后端 Java Controller 中的 `@RequestMapping("/api/...")` 对应)。
|
|
||||||
- 示例:
|
|
||||||
```ts
|
|
||||||
// 正确
|
|
||||||
const res = await api.get('/api/assets/list', { params })
|
|
||||||
|
|
||||||
// 错误(不要省略 /api)
|
|
||||||
const res = await api.get('/assets/list', { params })
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. API封装规范
|
|
||||||
- 统一使用项目中已封装的 `api` 实例(通常位于 `src/api/index.ts` 或 `src/utils/request.ts`)。
|
|
||||||
- 不要直接使用 `axios` 创建新实例。
|
|
||||||
- 请求方法统一使用:
|
|
||||||
- `GET`、`POST`、`PUT`、`DELETE`
|
|
||||||
- 复杂查询优先使用 `POST` + 请求体。
|
|
||||||
|
|
||||||
## 3. 类型定义要求
|
|
||||||
- 所有请求和响应必须定义 **TypeScript 接口**(放在 `src/types/` 或对应模块的 `types.ts` 中)。
|
|
||||||
- 命名规范:
|
|
||||||
- 请求:`ListAssetsParams`、`CreateAssetRequest`
|
|
||||||
- 响应:`ListAssetsResponse`、`ApiResponse<T>`
|
|
||||||
- 示例:
|
|
||||||
```ts
|
|
||||||
export interface ListAssetsParams {
|
|
||||||
isTop?: 'Y' | 'N'
|
|
||||||
beginTime?: string
|
|
||||||
endTime?: string
|
|
||||||
pageNum?: number
|
|
||||||
pageSize?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListAssetsResponse {
|
|
||||||
code: number
|
|
||||||
data: {
|
|
||||||
list: AssetItem[]
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 错误处理规范
|
|
||||||
- 使用统一的错误处理拦截器(项目已有全局拦截)。
|
|
||||||
- 业务错误统一抛出或使用 `ElMessage.error()` / `a-message`。
|
|
||||||
- 网络错误、超时、401/403 等状态码必须有清晰的用户提示。
|
|
||||||
- 重要操作(删除、收藏等)必须添加二次确认。
|
|
||||||
|
|
||||||
## 5. 其他最佳实践
|
|
||||||
- 使用 `async/await`,避免 `.then().catch()` 链式调用。
|
|
||||||
- Loading 状态统一使用 `loading` 变量 + Ant Design `a-spin`。
|
|
||||||
- 分页相关参数统一使用 `pageNum` / `pageSize`。
|
|
||||||
- 所有API调用必须添加中文注释说明用途。
|
|
||||||
|
|
||||||
**所有前端与后端交互的代码必须严格遵守以上规则。**
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
---
|
|
||||||
description: Vue 3 组件开发规范(portal-ui 项目专用)
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Vue组件开发规范(portal-ui)
|
|
||||||
|
|
||||||
## 1. 文件结构
|
|
||||||
所有 `.vue` 文件必须严格遵循以下结构:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<!-- 模板部分 -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// 1. imports
|
|
||||||
// 2. props / emits
|
|
||||||
// 3. 响应式数据
|
|
||||||
// 4. computed / watch
|
|
||||||
// 5. 生命周期
|
|
||||||
// 6. 方法
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
// 样式(必须与现有风格一致)
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 命名规范
|
|
||||||
- 组件文件名和组件名:**PascalCase**(如 `AssetPreviewModal.vue`)
|
|
||||||
- 事件名:kebab-case(如 `update:visible`、`preview-open`)
|
|
||||||
- Props:camelCase 定义
|
|
||||||
- 组合式函数:`useXXX.ts`(如 `useAssetList.ts`)
|
|
||||||
|
|
||||||
## 3. 技术栈要求
|
|
||||||
- **优先使用** `<script setup lang="ts">`
|
|
||||||
- 使用 **Ant Design Vue** 组件(`a-table`、`a-modal`、`a-select` 等)
|
|
||||||
- 状态管理优先使用 `ref` / `reactive`,复杂状态可使用 Pinia
|
|
||||||
- API调用必须导入并使用统一封装的 `api` 实例
|
|
||||||
- 类型定义必须独立或放在 `types.ts` 中
|
|
||||||
|
|
||||||
## 4. 代码质量要求
|
|
||||||
- 组件单一职责,体积过大时进行拆分
|
|
||||||
- 复杂逻辑抽离到 `composables/` 目录
|
|
||||||
- 所有用户可见文字使用中文
|
|
||||||
- 重要操作添加 `loading` 状态和 `confirm` 确认
|
|
||||||
- 图片/视频预览必须复用现有 `.preview-content` 样式
|
|
||||||
- 必须添加必要的中文注释
|
|
||||||
|
|
||||||
## 5. 性能与最佳实践
|
|
||||||
- 列表渲染使用 `:key`
|
|
||||||
- 避免模板中复杂表达式,使用 `computed`
|
|
||||||
- 大量数据考虑虚拟滚动
|
|
||||||
- 弹窗使用 `visible` + `emit` 控制
|
|
||||||
|
|
||||||
**所有新Vue组件必须同时遵守 `vue-ui-style.mdc` 和本规则,保持与 GeneratedAssets.vue 完全一致的视觉风格。**
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
---
|
|
||||||
description: portal-ui UI 风格保持规则(暗黑实验室风 / Dark Lab)
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# portal-ui 暗黑实验室风格规则
|
|
||||||
|
|
||||||
**核心原则:深灰底(禁用纯黑 `#000`)、半透明毛玻璃卡片、极细发光描边、生成区点阵/网格背景、主按钮霓虹淡蓝紫渐变、提示词输入使用 `PromptHighlightTextarea` 高亮常见质量词。**
|
|
||||||
|
|
||||||
## 1. 颜色与质感(`var.less` & `base.less`)
|
|
||||||
|
|
||||||
- **页面背景**:`#0D0D0D` / `#111111`,禁止 `#000`
|
|
||||||
- **卡片/输入**:`rgba(20,20,20,0.8)` + `backdrop-filter: blur` + `1px` 边框 `rgba(140,180,255,0.25~0.3)` + 微弱外发光
|
|
||||||
- **主色**:Arco `arcoblue` 调整为偏蓝紫(如 `#8899ff`);主按钮使用 **线性渐变**(淡蓝 → 淡紫)+ 柔和光晕
|
|
||||||
- **主字体**:`Inter`(`index.html` 已引入),中文回退 `PingFang SC` / `Microsoft YaHei`
|
|
||||||
- **生成画布区**:使用工具类 `.lab-canvas-bg`(约 5% 透明度的点阵 + 网格)
|
|
||||||
|
|
||||||
## 2. 布局与组件
|
|
||||||
|
|
||||||
- 主布局根节点:`default.vue` 使用 `class="app-wrapper dark-lab"`;全局覆盖在 `base.less` 的 `.dark-lab ...` 与 `.lab-*` 工具类
|
|
||||||
- 导航/侧栏:`navBar.vue`、`sideBar.vue` 与毛玻璃顶栏、侧栏一致
|
|
||||||
- 模态:`arco-modal-simple` 等与发光细边框一致,避免高饱和粉边
|
|
||||||
|
|
||||||
## 3. 提示词高亮
|
|
||||||
|
|
||||||
- 文生图等场景的提示词输入优先使用 `src/components/PromptHighlightTextarea.vue`(`v-model` 绑定字符串)
|
|
||||||
- 触发词列表维护在组件内(如 `cinematic`、`8k`、`masterpiece` 等),更新时保持 XSS 安全(先 escape 再包 `<span>`)
|
|
||||||
|
|
||||||
## 4. 开发要求
|
|
||||||
|
|
||||||
- 新页面卡片/表单区域与 `.dark-lab` 下 Arco 覆盖一致;局部可用与 `GeneratedImages.vue` 相同的 glass 块样式
|
|
||||||
- 预览图:细描边 + 深阴影浮起,避免粗黑硬边
|
|
||||||
- 修改全局主题后执行 `npm run dev` / `npm run build` 做回归
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
######################################################################
|
|
||||||
# Build Tools
|
|
||||||
|
|
||||||
.gradle
|
|
||||||
/build/
|
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
target/
|
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
# IDE
|
|
||||||
|
|
||||||
### STS ###
|
|
||||||
.apt_generated
|
|
||||||
.classpath
|
|
||||||
.factorypath
|
|
||||||
.project
|
|
||||||
.settings
|
|
||||||
.springBeans
|
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
|
||||||
.idea
|
|
||||||
*.iws
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
|
|
||||||
### JRebel ###
|
|
||||||
rebel.xml
|
|
||||||
|
|
||||||
### NetBeans ###
|
|
||||||
nbproject/private/
|
|
||||||
build/*
|
|
||||||
nbbuild/
|
|
||||||
dist/
|
|
||||||
nbdist/
|
|
||||||
.nb-gradle/
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
# Others
|
|
||||||
*.log
|
|
||||||
*.xml.versionsBackup
|
|
||||||
*.swp
|
|
||||||
!*/dist.zip
|
|
||||||
!*/build/*.java
|
|
||||||
!*/build/*.html
|
|
||||||
!*/build/*.xml
|
|
||||||
|
|
@ -8,12 +8,7 @@
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
href="/images/logo.png" />
|
href="/images/logo.png" />
|
||||||
<title>纳绘香蕉</title>
|
<title>asio</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
created() {
|
created() {
|
||||||
this.$auth.toggleTheme()
|
this.$auth.toggleTheme()
|
||||||
document.body.setAttribute('arco-theme', 'dark')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI生成相关API
|
|
||||||
* 符合 vue-api-calling.mdc 规范
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function generateImage(data) {
|
|
||||||
return request({
|
|
||||||
url: '/api/ai/promptToImg',
|
|
||||||
method: 'post',
|
|
||||||
data: 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',
|
|
||||||
method: 'post',
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAiManagerInfo(type) {
|
|
||||||
return request({
|
|
||||||
url: '/api/manager/selectInfo',
|
|
||||||
method: 'get',
|
|
||||||
params: { type }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAiTags(parentId) {
|
|
||||||
return request({
|
|
||||||
url: '/api/tag/list',
|
|
||||||
method: 'get',
|
|
||||||
params: { parentId }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NanoBanana 专用回调地址(供调试使用)
|
|
||||||
export const NANO_CALLBACK_URL = import.meta.env.VITE_NANO_CALLBACK_URL || 'https://your-domain.com/api/ai/nano-callback'
|
|
||||||
|
|
@ -17,33 +17,31 @@ body {
|
||||||
body {
|
body {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
color: rgba(232, 236, 245, 0.92);
|
color: #1f2329;
|
||||||
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
|
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #0d0d0d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// firefox 滚动条样式
|
// firefox 滚动条样式
|
||||||
* {
|
* {
|
||||||
scrollbar-color: rgba(136, 153, 255, 0.45) #141418;
|
scrollbar-color: rgba(144, 147, 153, 0.5) #0f0f12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*滚动条整体样式*/
|
/*滚动条整体样式*/
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
background-color: #141418;
|
background-color: #0f0f12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*滚动条里面小方块*/
|
/*滚动条里面小方块*/
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: linear-gradient(180deg, rgba(120, 170, 255, 0.55), rgba(160, 130, 255, 0.5));
|
background-color: red;
|
||||||
border: 2px solid #141418;
|
border: 3px solid #0f0f12;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*滚动条里面轨道*/
|
/*滚动条里面轨道*/
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #141418;
|
background: #0f0f12;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
|
|
@ -242,17 +240,14 @@ input:-webkit-autofill {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
background: #111111;
|
background: #fff;
|
||||||
padding: 12px !important;
|
padding: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-modal-simple {
|
.arco-modal-simple {
|
||||||
background: rgba(20, 20, 20, 0.92);
|
background: #0f0f12;
|
||||||
backdrop-filter: blur(14px);
|
border-radius: 10px;
|
||||||
-webkit-backdrop-filter: blur(14px);
|
border: 2px solid #e6217a;
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.28);
|
|
||||||
box-shadow: 0 0 40px rgba(60, 100, 200, 0.15);
|
|
||||||
.arco-modal-title {
|
.arco-modal-title {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
@ -277,249 +272,3 @@ input:-webkit-autofill {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ====================== 暗黑实验室风 (Dark Lab) ====================== */
|
|
||||||
|
|
||||||
.lab-glass() {
|
|
||||||
background: rgba(20, 20, 20, 0.8) !important;
|
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
-webkit-backdrop-filter: blur(14px);
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.28) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(120, 170, 255, 0.08),
|
|
||||||
0 0 28px rgba(80, 120, 220, 0.12) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
letter-spacing: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
background: #0d0d0d;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
|
|
||||||
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
|
||||||
background-size:
|
|
||||||
12px 12px,
|
|
||||||
24px 24px,
|
|
||||||
24px 24px;
|
|
||||||
background-position: 0 0, 0 0, 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
.arco-typography,
|
|
||||||
.title {
|
|
||||||
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
line-height: 1.25;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: rgba(245, 247, 252, 0.96);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 生成画布:5% 层级点阵 + 网格(类可单独用于右侧预览区) */
|
|
||||||
.lab-canvas-bg {
|
|
||||||
background-color: #111111 !important;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
|
||||||
linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px) !important;
|
|
||||||
background-size: 14px 14px, 22px 22px, 22px 22px !important;
|
|
||||||
background-position: 0 0, 0 0, 0 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lab-glass-inner {
|
|
||||||
.lab-glass();
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(120, 170, 255, 0.12),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .app-wrapper,
|
|
||||||
.dark-lab,
|
|
||||||
.app-wrapper.dark-lab {
|
|
||||||
background: #0d0d0d !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
color: rgba(232, 236, 245, 0.92) !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-card,
|
|
||||||
.dark-lab .mf-pane,
|
|
||||||
.dark-lab .card,
|
|
||||||
.dark-lab .panel {
|
|
||||||
.lab-glass();
|
|
||||||
color: rgba(232, 236, 245, 0.92) !important;
|
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-btn:not(.arco-btn-primary),
|
|
||||||
.dark-lab .mf-button:not([type='primary']) {
|
|
||||||
background: rgba(28, 28, 32, 0.9) !important;
|
|
||||||
color: rgba(232, 236, 245, 0.92) !important;
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.22) !important;
|
|
||||||
box-shadow: 0 0 18px rgba(70, 100, 200, 0.08) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-btn-primary,
|
|
||||||
.dark-lab .mf-button[type='primary'],
|
|
||||||
.dark-lab .mf-button.arco-btn-primary {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(130, 185, 255, 0.95) 0%,
|
|
||||||
rgba(175, 145, 255, 0.92) 100%
|
|
||||||
) !important;
|
|
||||||
color: #0d0d0d !important;
|
|
||||||
border: 1px solid rgba(200, 220, 255, 0.45) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 0 24px rgba(120, 170, 255, 0.35),
|
|
||||||
0 0 48px rgba(160, 130, 255, 0.15) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-btn-primary:hover,
|
|
||||||
.dark-lab .mf-button[type='primary']:hover {
|
|
||||||
filter: brightness(1.06);
|
|
||||||
box-shadow:
|
|
||||||
0 0 32px rgba(130, 185, 255, 0.45),
|
|
||||||
0 0 56px rgba(160, 130, 255, 0.22) !important;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-input-wrapper,
|
|
||||||
.dark-lab .arco-textarea-wrapper,
|
|
||||||
.dark-lab .arco-select-view-single,
|
|
||||||
.dark-lab .arco-select,
|
|
||||||
.dark-lab .input-wrapper,
|
|
||||||
.dark-lab input:not([type='checkbox']):not([type='radio']),
|
|
||||||
.dark-lab select,
|
|
||||||
.dark-lab textarea {
|
|
||||||
.lab-glass();
|
|
||||||
color: rgba(232, 236, 245, 0.92) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(120, 170, 255, 0.1),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-menu {
|
|
||||||
background: rgba(18, 18, 22, 0.92) !important;
|
|
||||||
border-right: 1px solid rgba(140, 180, 255, 0.18) !important;
|
|
||||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.35) !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-menu-item {
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
margin: 4px 8px !important;
|
|
||||||
padding: 12px 16px !important;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
color: rgba(200, 210, 230, 0.88) !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-menu-item:hover,
|
|
||||||
.dark-lab .arco-menu-item.arco-menu-selected {
|
|
||||||
background: rgba(136, 153, 255, 0.12) !important;
|
|
||||||
color: #e8eeff !important;
|
|
||||||
box-shadow: 0 0 20px rgba(120, 170, 255, 0.12) !important;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-modal-content,
|
|
||||||
.dark-lab .arco-drawer-content {
|
|
||||||
.lab-glass();
|
|
||||||
border-radius: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .arco-modal-header,
|
|
||||||
.dark-lab .arco-modal-title {
|
|
||||||
background: rgba(14, 14, 18, 0.95) !important;
|
|
||||||
color: rgba(245, 247, 252, 0.96) !important;
|
|
||||||
border-bottom: 1px solid rgba(140, 180, 255, 0.2) !important;
|
|
||||||
text-transform: none;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .navbar {
|
|
||||||
background: rgba(14, 14, 18, 0.85) !important;
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
border-bottom: 1px solid rgba(140, 180, 255, 0.2) !important;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .right-menu-item.user {
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.22) !important;
|
|
||||||
background: rgba(20, 20, 24, 0.75) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
box-shadow: 0 0 18px rgba(80, 120, 200, 0.1) !important;
|
|
||||||
color: rgba(232, 236, 245, 0.92) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asymmetric-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asymmetric-sidebar {
|
|
||||||
margin-right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: 0;
|
|
||||||
padding: 20px 28px;
|
|
||||||
background: transparent;
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览图:柔化边框,略浮起 */
|
|
||||||
.dark-lab img:not(.logo img):not(.wallet img),
|
|
||||||
.dark-lab .arco-image img,
|
|
||||||
.dark-lab .preview-image {
|
|
||||||
filter: none;
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.2) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 12px 40px rgba(0, 0, 0, 0.55),
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.04) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
image-rendering: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accent {
|
|
||||||
color: #a8bfff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-lab .logo-wrap {
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.28);
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 20px rgba(100, 140, 220, 0.12);
|
|
||||||
background: rgba(20, 20, 24, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 兼容:未挂 dark-lab 的裸组件仍使用实验室输入质感 */
|
|
||||||
.arco-input-wrapper,
|
|
||||||
.arco-textarea-wrapper {
|
|
||||||
border-radius: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
body {
|
body {
|
||||||
/* 暗黑实验室:深底 + 霓虹蓝紫主色 */
|
// 背景色 - 浅
|
||||||
--mf-color-bg-3: #111111;
|
--mf-color-bg-3: #f8f8fa;
|
||||||
--his-border-color: rgba(140, 180, 255, 0.28);
|
--his-border-color: rgb(166, 124, 82, 0.3);
|
||||||
--primary-6: 136 153 255; /* #8899ff */
|
|
||||||
--color-text-1: rgba(232, 236, 245, 0.94);
|
|
||||||
--color-text-2: rgba(200, 210, 230, 0.85);
|
|
||||||
--color-bg-1: #0d0d0d;
|
|
||||||
--color-bg-2: #141418;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body[arco-theme='dark'] {
|
body[arco-theme='dark'] {
|
||||||
--mf-color-bg-3: #141418;
|
// 背景色 - 浅
|
||||||
--primary-6: 136 153 255;
|
--mf-color-bg-3: #17171a;
|
||||||
--color-text-1: rgba(232, 236, 245, 0.94);
|
|
||||||
--color-bg-1: #0d0d0d;
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
<template>
|
||||||
|
<div :class="prefixCls">
|
||||||
|
<mf-dialog
|
||||||
|
:visible="visible"
|
||||||
|
:footer="false"
|
||||||
|
hideTitle
|
||||||
|
unmountOnClose
|
||||||
|
fullscreen
|
||||||
|
:class="`${prefixCls}-wrapper`"
|
||||||
|
:modal-class="`${prefixCls}-dialog`">
|
||||||
|
<mf-video
|
||||||
|
autoplay
|
||||||
|
:controls="false"
|
||||||
|
modelValue="https://images.iqyjsnwv.com/tmp/hello-BCSGJ8fP.mp4" />
|
||||||
|
<div :class="`${prefixCls}-shadow`">
|
||||||
|
<div :class="`${prefixCls}-title`">
|
||||||
|
{{ $t('common.fbTitle') }}
|
||||||
|
</div>
|
||||||
|
<div :class="`${prefixCls}-content`">
|
||||||
|
{{ $t('common.fbContent') }}
|
||||||
|
</div>
|
||||||
|
<div :class="`${prefixCls}-footer`">
|
||||||
|
<mf-button
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
@click="ok">
|
||||||
|
{{ $t('common.fbOK') }}
|
||||||
|
</mf-button>
|
||||||
|
<mf-button
|
||||||
|
size="large"
|
||||||
|
@click="cancel">
|
||||||
|
{{ $t('common.fbCancel') }}
|
||||||
|
</mf-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mf-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'mf-forbidden',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
prefixCls: 'mf-forbidden'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
visible: Boolean
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancel() {
|
||||||
|
this.$router.replace('/403')
|
||||||
|
},
|
||||||
|
ok() {
|
||||||
|
this.$store.dispatch('main/setForbidden', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.mf-forbidden {
|
||||||
|
overflow: hidden;
|
||||||
|
&-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
.arco-modal-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.arco-modal-mask {
|
||||||
|
/* 背景高斯模糊关键属性 */
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-dialog {
|
||||||
|
overflow: hidden;
|
||||||
|
// width: 428px;
|
||||||
|
// height: 258px;
|
||||||
|
// background: linear-gradient(0deg, #271433 0%, #e6217a 49%);
|
||||||
|
// border-radius: 20px;
|
||||||
|
// border: 2px solid #e6217a;
|
||||||
|
// top: 45% !important;
|
||||||
|
// transform: translateY(-45%) !important;
|
||||||
|
|
||||||
|
.arco-modal-body {
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
.arco-spin {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
.mf-dialog-wrap {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-video {
|
||||||
|
position: absolute;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-top: 40px;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-shadow {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 60px;
|
||||||
|
flex-direction: column;
|
||||||
|
.mf-button {
|
||||||
|
width: 300px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 25px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
&:hover {
|
||||||
|
background-color: #262626;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #0d0d0d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.mf-forbidden {
|
||||||
|
&-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-dialog {
|
||||||
|
.mf-video {
|
||||||
|
video {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="prompt-highlight-wrap lab-glass-inner">
|
|
||||||
<div
|
|
||||||
class="prompt-highlight-backdrop"
|
|
||||||
aria-hidden="true">
|
|
||||||
<div
|
|
||||||
class="prompt-highlight-backdrop-inner"
|
|
||||||
:style="backdropShift"
|
|
||||||
v-html="highlightedHtml"></div>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
ref="ta"
|
|
||||||
class="prompt-highlight-input"
|
|
||||||
:value="modelValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:rows="rows"
|
|
||||||
:disabled="disabled"
|
|
||||||
spellcheck="false"
|
|
||||||
@input="onInput"
|
|
||||||
@scroll="onScroll" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const KEYWORDS = [
|
|
||||||
'4k uhd',
|
|
||||||
'ultra hd',
|
|
||||||
'highly detailed',
|
|
||||||
'unreal engine',
|
|
||||||
'octane render',
|
|
||||||
'ray tracing',
|
|
||||||
'film grain',
|
|
||||||
'best quality',
|
|
||||||
'photorealistic',
|
|
||||||
'masterpiece',
|
|
||||||
'cinematic',
|
|
||||||
'8k',
|
|
||||||
'4k',
|
|
||||||
'2k',
|
|
||||||
'hdr',
|
|
||||||
'bokeh',
|
|
||||||
'uhd',
|
|
||||||
'dslr',
|
|
||||||
'volumetric'
|
|
||||||
]
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
if (!s) return ''
|
|
||||||
return String(s)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
}
|
|
||||||
|
|
||||||
let keywordPattern
|
|
||||||
function getKeywordPattern() {
|
|
||||||
if (!keywordPattern) {
|
|
||||||
const parts = KEYWORDS.map((k) =>
|
|
||||||
k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
).sort((a, b) => b.length - a.length)
|
|
||||||
keywordPattern = new RegExp(`\\b(${parts.join('|')})\\b`, 'gi')
|
|
||||||
}
|
|
||||||
return keywordPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PromptHighlightTextarea',
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
rows: {
|
|
||||||
type: Number,
|
|
||||||
default: 4
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
scrollTop: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
highlightedHtml() {
|
|
||||||
const raw = this.modelValue
|
|
||||||
if (!raw) return ''
|
|
||||||
const escaped = escapeHtml(raw)
|
|
||||||
return escaped.replace(
|
|
||||||
getKeywordPattern(),
|
|
||||||
'<span class="prompt-hl-keyword">$&</span>'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backdropShift() {
|
|
||||||
return {
|
|
||||||
transform: `translateY(-${this.scrollTop}px)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onInput(e) {
|
|
||||||
this.$emit('update:modelValue', e.target.value)
|
|
||||||
},
|
|
||||||
onScroll(e) {
|
|
||||||
this.scrollTop = e.target.scrollTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.prompt-highlight-wrap {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 96px;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-highlight-backdrop {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-highlight-backdrop-inner {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5715;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
color: rgba(232, 236, 245, 0.92);
|
|
||||||
tab-size: 4;
|
|
||||||
|
|
||||||
:deep(.prompt-hl-keyword) {
|
|
||||||
color: #9db7ff;
|
|
||||||
font-weight: 600;
|
|
||||||
text-shadow: 0 0 14px rgba(140, 180, 255, 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-highlight-input {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 96px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5715;
|
|
||||||
color: transparent;
|
|
||||||
caret-color: #b8c9ff;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
resize: vertical;
|
|
||||||
outline: none;
|
|
||||||
tab-size: 4;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(180, 190, 210, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export { default as List } from './List.vue';
|
export { default as List } from './List.vue';
|
||||||
export { default as Card } from './Card.vue';
|
export { default as Card } from './Card.vue';
|
||||||
export { default as PromptHighlightTextarea } from './PromptHighlightTextarea.vue';
|
|
||||||
export { default as NumberGroup } from './NumberGroup.vue';
|
export { default as NumberGroup } from './NumberGroup.vue';
|
||||||
|
export { default as Forbidden } from './Forbidden.vue';
|
||||||
export { default as RechargeSelect } from './RechargeSelect.vue';
|
export { default as RechargeSelect } from './RechargeSelect.vue';
|
||||||
export { default as RechargePc } from './RechargePc.vue';
|
export { default as RechargePc } from './RechargePc.vue';
|
||||||
export { default as Preview } from './Preview.vue';
|
export { default as Preview } from './Preview.vue';
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,6 @@ 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',
|
||||||
|
|
@ -42,6 +37,10 @@ export default {
|
||||||
rechartTip1: 'When recharging via wallet transfer, please make sure the wallet is on the correct blockchain network!!!',
|
rechartTip1: 'When recharging via wallet transfer, please make sure the wallet is on the correct blockchain network!!!',
|
||||||
rechartTip2: 'Recharge may be delayed. Please wait 3–5 minutes before refreshing to check.',
|
rechartTip2: 'Recharge may be delayed. Please wait 3–5 minutes before refreshing to check.',
|
||||||
walletAddr: 'Wallet Address:',
|
walletAddr: 'Wallet Address:',
|
||||||
|
fbTitle: 'Warning! This website is for adults only!',
|
||||||
|
fbContent: 'By entering, you confirm you are 18+.',
|
||||||
|
fbCancel: 'Under 18',
|
||||||
|
fbOK: 'I’m 18+',
|
||||||
sorry: 'Sorry!',
|
sorry: 'Sorry!',
|
||||||
useLess: 'You cannot use this website...',
|
useLess: 'You cannot use this website...',
|
||||||
loginAccount: 'Login Account',
|
loginAccount: 'Login Account',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
index: 'Home',
|
index: 'Home',
|
||||||
imageToImage: 'Image-to-Image',
|
imageToImage: 'One-click undressing',
|
||||||
imageToImage2: 'Image-to-Image2',
|
imageToImage2: 'Image-to-Image2',
|
||||||
changeFace: 'Swap Face',
|
changeFace: 'Swap Face',
|
||||||
changeFaceVideo: 'Swap Video Face',
|
changeFaceVideo: 'Swap Video Face',
|
||||||
|
|
@ -8,6 +8,5 @@ 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,13 +2,25 @@ 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({
|
||||||
|
|
@ -16,7 +28,13 @@ 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,11 +14,6 @@ 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: '視頻換臉',
|
||||||
|
|
@ -42,6 +37,10 @@ export default {
|
||||||
rechartTip1: '使用錢包進行轉帳充值時,請務必確認轉帳錢包所屬的鏈網路!!!',
|
rechartTip1: '使用錢包進行轉帳充值時,請務必確認轉帳錢包所屬的鏈網路!!!',
|
||||||
rechartTip2: '充值入帳可能會有延遲,請稍等 3-5 分鐘後再刷新查詢',
|
rechartTip2: '充值入帳可能會有延遲,請稍等 3-5 分鐘後再刷新查詢',
|
||||||
walletAddr: '錢包地址:',
|
walletAddr: '錢包地址:',
|
||||||
|
fbTitle: '警告!此網站僅適合成年人!',
|
||||||
|
fbContent: '進入本網站即表示我確認已年滿 18 歲或以上',
|
||||||
|
fbCancel: '我未滿 18',
|
||||||
|
fbOK: '我已滿 18',
|
||||||
sorry: '抱歉!',
|
sorry: '抱歉!',
|
||||||
useLess: '您無法使用該網站...',
|
useLess: '您無法使用該網站...',
|
||||||
loginAccount: '登入帳號',
|
loginAccount: '登入帳號',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
index: '首頁',
|
index: '首頁',
|
||||||
imageToImage: '图生图',
|
imageToImage: '一鍵脫衣',
|
||||||
imageToImage2: '圖生圖2',
|
imageToImage2: '圖生圖2',
|
||||||
changeFace: '一鍵換臉',
|
changeFace: '一鍵換臉',
|
||||||
changeFaceVideo: '視頻換臉',
|
changeFaceVideo: '視頻換臉',
|
||||||
|
|
@ -8,6 +8,5 @@ export default {
|
||||||
fastVideo: '快捷生視頻',
|
fastVideo: '快捷生視頻',
|
||||||
recharge: '快速充值',
|
recharge: '快速充值',
|
||||||
help: '幫助中心',
|
help: '幫助中心',
|
||||||
moneyInvite: '有獎邀請',
|
moneyInvite: '有獎邀請'
|
||||||
'AI文生图': 'AI 文生圖'
|
|
||||||
}
|
}
|
||||||
|
|
@ -203,10 +203,13 @@ export default {
|
||||||
.login-dialog {
|
.login-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(180deg, rgba(20, 20, 20, 0.92) 0%, rgba(13, 13, 13, 0.92) 100%);
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(39, 20, 51, 0.7) 0%,
|
||||||
|
rgba(230, 33, 122, 0.7) 49%
|
||||||
|
);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.3);
|
border: 2px solid #e6217a;
|
||||||
box-shadow: 0 0 24px rgba(136, 153, 255, 0.12);
|
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 320px;
|
height: 320px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
|
|
@ -278,7 +281,7 @@ export default {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.28);
|
border: 1px solid rgba(#ffffff, 0.3);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
@ -300,7 +303,7 @@ export default {
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: rgba(140, 180, 255, 0.6);
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-image {
|
.arco-image {
|
||||||
|
|
@ -328,7 +331,7 @@ export default {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #252525;
|
background-color: #262626;
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
background-color: #0d0d0d;
|
background-color: #0d0d0d;
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,14 @@ const openedKeys = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const menuItemClick = (key) => {
|
const menuItemClick = (key) => {
|
||||||
|
if (key == 'change-face-video') {
|
||||||
|
$message.warning(generateLang('isDevelop'))
|
||||||
|
}else {
|
||||||
router.push({ name: key })
|
router.push({ name: key })
|
||||||
if ($base.isMobile()) {
|
if ($base.isMobile()) {
|
||||||
store.dispatch('main/closeSideBar')
|
store.dispatch('main/closeSideBar')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const subMenuClick = (key, _openKeys) => {
|
const subMenuClick = (key, _openKeys) => {
|
||||||
|
|
|
||||||
|
|
@ -265,11 +265,14 @@ export default {
|
||||||
.register-dialog {
|
.register-dialog {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(180deg, rgba(20, 20, 20, 0.92) 0%, rgba(13, 13, 13, 0.92) 100%);
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(39, 20, 51, 0.7) 0%,
|
||||||
|
rgba(230, 33, 122, 0.7) 49%
|
||||||
|
);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.3);
|
border: 2px solid #e6217a;
|
||||||
box-shadow: 0 0 24px rgba(136, 153, 255, 0.12);
|
|
||||||
width: 500px;
|
width: 500px;
|
||||||
top: 45% !important;
|
top: 45% !important;
|
||||||
transform: translateY(-45%) !important;
|
transform: translateY(-45%) !important;
|
||||||
|
|
@ -340,7 +343,7 @@ export default {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.28);
|
border: 1px solid rgba(#ffffff, 0.3);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
@ -362,8 +365,7 @@ export default {
|
||||||
|
|
||||||
&-send {
|
&-send {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #ffffff;
|
color: #000000;
|
||||||
background: linear-gradient(90deg, #7f97ff 0%, #9f8bff 100%);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +376,7 @@ export default {
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: rgba(140, 180, 255, 0.6);
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-image {
|
.arco-image {
|
||||||
|
|
@ -402,7 +404,7 @@ export default {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #252525;
|
background-color: #262626;
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
background-color: #0d0d0d;
|
background-color: #0d0d0d;
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@
|
||||||
@cancel="userVisible = false"
|
@cancel="userVisible = false"
|
||||||
:visible="userVisible" />
|
:visible="userVisible" />
|
||||||
|
|
||||||
|
<mf-forbidden :visible="showForbidden" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -135,6 +136,7 @@ export default {
|
||||||
'theme',
|
'theme',
|
||||||
'permission_routes',
|
'permission_routes',
|
||||||
'lang',
|
'lang',
|
||||||
|
'showForbidden',
|
||||||
'showLogin',
|
'showLogin',
|
||||||
'sidebar'
|
'sidebar'
|
||||||
]),
|
]),
|
||||||
|
|
@ -244,14 +246,8 @@ 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: 24px;
|
padding-left: 30px;
|
||||||
padding-right: 24px;
|
padding-right: 30px;
|
||||||
height: 72px;
|
|
||||||
background: rgba(14, 14, 18, 0.85);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
border-bottom: 1px solid rgba(140, 180, 255, 0.2);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
|
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -260,9 +256,7 @@ export default {
|
||||||
|
|
||||||
&-collapse {
|
&-collapse {
|
||||||
display: none;
|
display: none;
|
||||||
transition: 0.2s ease;
|
transition: 0.25s;
|
||||||
color: rgba(200, 215, 245, 0.9);
|
|
||||||
font-size: 28px;
|
|
||||||
|
|
||||||
&.isCollapse {
|
&.isCollapse {
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
|
|
@ -273,45 +267,34 @@ export default {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 72px;
|
height: 60px;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: rgba(245, 247, 252, 0.96);
|
color: var(--color-text-1);
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
font-size: 22px;
|
|
||||||
|
|
||||||
&-wrap {
|
&-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 12px;
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.28);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 20px rgba(100, 140, 220, 0.12);
|
|
||||||
background: rgba(20, 20, 24, 0.65);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu {
|
.right-menu {
|
||||||
height: 72px;
|
height: 60px;
|
||||||
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: 48px;
|
height: 60px;
|
||||||
margin-right: 8px;
|
margin-right: 12px;
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
@ -320,53 +303,44 @@ export default {
|
||||||
&.language {
|
&.language {
|
||||||
.mf-button {
|
.mf-button {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(220, 228, 245, 0.92);
|
color: #999999;
|
||||||
background: rgba(28, 28, 32, 0.85);
|
background-color: transparent;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.22);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 18px rgba(80, 120, 200, 0.08);
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(136, 153, 255, 0.18);
|
background-color: transparent;
|
||||||
color: #fff;
|
|
||||||
border-color: rgba(180, 200, 255, 0.4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.logout {
|
&.logout {
|
||||||
.mf-button {
|
.mf-button {
|
||||||
background: rgba(28, 28, 32, 0.85);
|
background-color: transparent;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.22);
|
|
||||||
color: rgba(220, 228, 245, 0.92);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 18px rgba(80, 120, 200, 0.08);
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(136, 153, 255, 0.18);
|
background-color: transparent;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login {
|
.login {
|
||||||
width: 110px;
|
width: 100px;
|
||||||
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.user {
|
&.user {
|
||||||
width: 170px;
|
width: 150px;
|
||||||
color: rgba(232, 236, 245, 0.92);
|
color: rgb(var(--primary-6));
|
||||||
height: 44px;
|
border: 2px solid transparent;
|
||||||
|
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: 12px;
|
padding-left: 16px;
|
||||||
padding-right: 8px;
|
padding-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
|
||||||
border: 1px solid rgba(140, 180, 255, 0.22);
|
|
||||||
background: rgba(20, 20, 24, 0.75);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 18px rgba(80, 120, 200, 0.1);
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -374,8 +348,6 @@ export default {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
.arco-image {
|
.arco-image {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -384,59 +356,54 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-point-name {
|
.nav-point-name {
|
||||||
color: rgba(232, 236, 245, 0.92);
|
color: var(--color-text-3);
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
width: 240px;
|
width: 220px;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.25);
|
|
||||||
background: rgba(18, 18, 22, 0.96);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
.arco-dropdown-option {
|
.arco-dropdown-option {
|
||||||
.mf-divider {
|
.mf-divider {
|
||||||
margin: 8px 0;
|
margin: 5px 0;
|
||||||
border-color: rgba(140, 180, 255, 0.15);
|
|
||||||
}
|
}
|
||||||
.user-info-wrap {
|
.user-info-wrap {
|
||||||
padding: 16px;
|
padding-top: 8px;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.12);
|
padding-bottom: 8px;
|
||||||
background: rgba(22, 22, 28, 0.9);
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
.mf-avatar {
|
.mf-avatar {
|
||||||
margin-right: 12px;
|
margin-right: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border: 1px solid rgba(140, 180, 255, 0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info-name {
|
.user-info-name {
|
||||||
width: 160px;
|
width: 150px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(245, 247, 252, 0.96);
|
color: var(--color-text-1);
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
letter-spacing: -0.02em;
|
margin-bottom: 0px;
|
||||||
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: 160px;
|
width: 150px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: rgba(200, 210, 230, 0.85);
|
color: var(--color-text-4);
|
||||||
font-weight: 500;
|
// margin-top: 3px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(136, 153, 255, 0.15);
|
background-color: transparent;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
@ -445,13 +412,11 @@ export default {
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
height: auto;
|
|
||||||
.left {
|
.left {
|
||||||
&-collapse {
|
&-collapse {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
color: rgba(200, 215, 245, 0.9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
&-menu {
|
||||||
|
|
@ -459,13 +424,15 @@ export default {
|
||||||
.logo {
|
.logo {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.mf-icon {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu-item {
|
.right-menu-item {
|
||||||
&.user {
|
&.user {
|
||||||
width: 140px;
|
width: 136px;
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,53 +110,65 @@ export default {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: width 0.15s ease;
|
transition: width 0.3s;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
background-color: rgba(14, 14, 18, 0.88);
|
background-color: #000;
|
||||||
backdrop-filter: blur(14px);
|
border-right: 1px solid; /* 宽度随你调整 */
|
||||||
-webkit-backdrop-filter: blur(14px);
|
border-image: linear-gradient(
|
||||||
border-right: 1px solid rgba(140, 180, 255, 0.15);
|
to bottom,
|
||||||
box-shadow: 4px 0 28px rgba(0, 0, 0, 0.35);
|
#0f0f12 0%,
|
||||||
|
rgba(255,255,255, 0.7) 40%,
|
||||||
|
rgba(255,255,255, 0.7) 60%,
|
||||||
|
#0f0f12 100%
|
||||||
|
)
|
||||||
|
1 100%;
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 56px !important;
|
width: 50px !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
padding-left: 4px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding-left: 8px;
|
padding-left: 16px;
|
||||||
border: none;
|
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: rgba(200, 210, 230, 0.88);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
border: none;
|
border-radius: 10px;
|
||||||
margin: 6px 4px;
|
margin-bottom: 12px;
|
||||||
padding: 12px 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: background 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&.arco-menu-selected,
|
&.arco-menu-selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(136, 153, 255, 0.14) !important;
|
background-color: rgb(var(--primary-6));
|
||||||
color: #f0f4ff !important;
|
|
||||||
transform: none;
|
|
||||||
box-shadow: 0 0 22px rgba(120, 170, 255, 0.12);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toogle-menu {
|
.toogle-menu {
|
||||||
display: none;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,11 +176,10 @@ export default {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 72px;
|
top: 60px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
height: calc(100% - 72px);
|
height: calc(100% - 60px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-right: 1px solid rgba(140, 180, 255, 0.15);
|
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app-wrapper dark-lab">
|
<div class="app-wrapper">
|
||||||
<div class="fixed-header">
|
<div class="fixed-header">
|
||||||
<nav-bar class="nav-bar" />
|
<nav-bar class="nav-bar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,25 +47,18 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0d0d0d;
|
background: #0f0f12;
|
||||||
|
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-bottom: 1px solid rgba(140, 180, 255, 0.15);
|
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
height: 72px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100% - 72px);
|
height: calc(100% - 60px);
|
||||||
.sidebar-wrapper {
|
|
||||||
border-right: 1px solid rgba(140, 180, 255, 0.12);
|
|
||||||
margin-right: 0;
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -69,15 +69,57 @@ export const constantRoutes = [{
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_tst'
|
icon: 'btn_tst'
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
path: 'image-to-image2',
|
// {
|
||||||
name: 'image-to-image2',
|
// path: 'image-to-image?type=2',
|
||||||
component: () => import('@/views/Image.vue'),
|
// 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'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'imageToImage2',
|
title: 'fastVideo',
|
||||||
menuItem: true,
|
menuItem: true,
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_tst'
|
icon: 'btn_kjsp'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'recharge',
|
path: 'recharge',
|
||||||
|
|
@ -157,7 +199,36 @@ function hasPermission({
|
||||||
|
|
||||||
// 权限控制
|
// 权限控制
|
||||||
router.beforeEach(async (to = {}, from, next) => {
|
router.beforeEach(async (to = {}, from, next) => {
|
||||||
|
if (from.path != '/fast-video') {
|
||||||
next()
|
next()
|
||||||
|
} else {
|
||||||
|
const lang = getLang()
|
||||||
|
const messages = i18n.global.messages[lang]
|
||||||
|
let isPrevent = store.getters.showPrevent;
|
||||||
|
if (isPrevent) {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: messages.common.notice,
|
||||||
|
content: messages.common.switchPageTip,
|
||||||
|
okText: messages.common.confirm,
|
||||||
|
cancelText: messages.common.cancel,
|
||||||
|
onOk: () => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
next(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
@ -43,6 +43,8 @@ const getters = {
|
||||||
messageData: (state) => state.main.messageData,
|
messageData: (state) => state.main.messageData,
|
||||||
// 未读消息数量
|
// 未读消息数量
|
||||||
messageCount: (state) => state.main.messageCount,
|
messageCount: (state) => state.main.messageCount,
|
||||||
|
// 是否显示18禁的弹窗
|
||||||
|
showForbidden: state => state.main.showForbidden,
|
||||||
// 是否阻止页面跳转
|
// 是否阻止页面跳转
|
||||||
showPrevent: state => state.main.showPrevent,
|
showPrevent: state => state.main.showPrevent,
|
||||||
showLogin: state => state.main.showLogin,
|
showLogin: state => state.main.showLogin,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ const state = {
|
||||||
topMenu: Cookies.get('topMenu'),
|
topMenu: Cookies.get('topMenu'),
|
||||||
// 当前语言
|
// 当前语言
|
||||||
language: Cookies.get('language') || 'en_US',
|
language: Cookies.get('language') || 'en_US',
|
||||||
// 主题 '':亮色 / dark:暗黑(默认暗黑实验室)
|
// 主题 '':亮色 / dark:暗黑
|
||||||
theme: $storage.get('theme') || 'dark',
|
theme: $storage.get('theme') || '',
|
||||||
// 系统端
|
// 系统端
|
||||||
system: $storage.get('system') || '',
|
system: $storage.get('system') || '',
|
||||||
// 当前系统的所有多语言数据
|
// 当前系统的所有多语言数据
|
||||||
|
|
@ -39,6 +39,8 @@ const state = {
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
messageData: {},
|
messageData: {},
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
|
// 是否显示18禁弹窗
|
||||||
|
showForbidden: true,
|
||||||
// 阻止页面切换
|
// 阻止页面切换
|
||||||
showPrevent: false,
|
showPrevent: false,
|
||||||
showLogin: false
|
showLogin: false
|
||||||
|
|
@ -129,6 +131,10 @@ const mutations = {
|
||||||
SET_UNREAD_MESSAGE(state, value) {
|
SET_UNREAD_MESSAGE(state, value) {
|
||||||
state.messageCount = value;
|
state.messageCount = value;
|
||||||
},
|
},
|
||||||
|
SET_FORBIDDEN(state, value) {
|
||||||
|
state.showForbidden = value;
|
||||||
|
$storage.set('showForbidden', value)
|
||||||
|
},
|
||||||
SET_PREVENT(state, value) {
|
SET_PREVENT(state, value) {
|
||||||
state.showPrevent = value;
|
state.showPrevent = value;
|
||||||
},
|
},
|
||||||
|
|
@ -248,6 +254,12 @@ const actions = {
|
||||||
commit('SET_UNREAD_MESSAGE', parseInt(res))
|
commit('SET_UNREAD_MESSAGE', parseInt(res))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// 设置是否显示18禁弹窗
|
||||||
|
setForbidden({
|
||||||
|
commit
|
||||||
|
}, value) {
|
||||||
|
commit('SET_FORBIDDEN', value)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="right lab-canvas-bg"
|
class="right"
|
||||||
v-if="showResult">
|
v-if="showResult">
|
||||||
<div class="right-close">
|
<div class="right-close">
|
||||||
<mf-icon
|
<mf-icon
|
||||||
|
|
@ -101,6 +101,14 @@
|
||||||
src="/images/btn_bctp@2x.png" />
|
src="/images/btn_bctp@2x.png" />
|
||||||
{{ $t('common.saveImage') }}
|
{{ $t('common.saveImage') }}
|
||||||
</mf-button>
|
</mf-button>
|
||||||
|
<mf-button @click="jumpToVideo">
|
||||||
|
<a-image
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
:preview="false"
|
||||||
|
src="/images/btn_scsp@2x.png" />
|
||||||
|
{{ $t('common.generateVideo') }}
|
||||||
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -182,6 +190,9 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
jumpToVideo() {
|
||||||
|
this.$router.push(`/fast-image?url=${this.imageUrl}`)
|
||||||
|
},
|
||||||
saveImage() {
|
saveImage() {
|
||||||
this.$file.downloadFile(this.imageUrl)
|
this.$file.downloadFile(this.imageUrl)
|
||||||
},
|
},
|
||||||
|
|
@ -218,7 +229,7 @@ export default {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
text: this.text || this.textPlaceholder,
|
text: this.text || this.textPlaceholder,
|
||||||
imageUrls: [this.firstUrl.url, this.secondUrl.url],
|
firstUrl: [this.firstUrl.url, this.secondUrl.url],
|
||||||
functionType: '13',
|
functionType: '13',
|
||||||
tags: tags.join(',')
|
tags: tags.join(',')
|
||||||
}
|
}
|
||||||
|
|
@ -369,6 +380,7 @@ export default {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background-color: #000000;
|
||||||
padding: 0 100px 20px 100px;
|
padding: 0 100px 20px 100px;
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="right lab-canvas-bg"
|
class="right"
|
||||||
v-if="showResult">
|
v-if="showResult">
|
||||||
<div class="right-close">
|
<div class="right-close">
|
||||||
<mf-icon
|
<mf-icon
|
||||||
|
|
@ -59,6 +59,14 @@
|
||||||
src="/images/btn_bctp@2x.png" />
|
src="/images/btn_bctp@2x.png" />
|
||||||
{{ $t('common.saveImage') }}
|
{{ $t('common.saveImage') }}
|
||||||
</mf-button>
|
</mf-button>
|
||||||
|
<mf-button @click="jumpToVideo">
|
||||||
|
<a-image
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
:preview="false"
|
||||||
|
src="/images/btn_scsp@2x.png" />
|
||||||
|
{{ $t('common.generateVideo') }}
|
||||||
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,6 +141,9 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
jumpToVideo() {
|
||||||
|
this.$router.push(`/fast-image?url=${this.imageUrl}`)
|
||||||
|
},
|
||||||
saveImage() {
|
saveImage() {
|
||||||
this.$file.downloadFile(this.imageUrl)
|
this.$file.downloadFile(this.imageUrl)
|
||||||
},
|
},
|
||||||
|
|
@ -275,6 +286,7 @@ export default {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background-color: #000000;
|
||||||
padding: 0 100px 20px 100px;
|
padding: 0 100px 20px 100px;
|
||||||
|
|
||||||
:deep(.arco-image-error) {
|
:deep(.arco-image-error) {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<div
|
<div
|
||||||
class="right lab-canvas-bg"
|
class="right"
|
||||||
v-if="showResult">
|
v-if="showResult">
|
||||||
<div class="right-close">
|
<div class="right-close">
|
||||||
<mf-icon
|
<mf-icon
|
||||||
|
|
@ -711,6 +711,7 @@ export default {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background-color: #000000;
|
||||||
padding: 20px 100px 20px 100px;
|
padding: 20px 100px 20px 100px;
|
||||||
|
|
||||||
:deep(.result-video) {
|
:deep(.result-video) {
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@
|
||||||
<mf-image-upload
|
<mf-image-upload
|
||||||
listType="draggable"
|
listType="draggable"
|
||||||
uploadHeight="200px"
|
uploadHeight="200px"
|
||||||
:multiple="true"
|
|
||||||
:limit="9"
|
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:content="$t('common.uploadPlaceholder')"
|
:content="$t('common.uploadPlaceholder')"
|
||||||
v-model="firstUrl" />
|
v-model="firstUrl" />
|
||||||
|
|
@ -70,54 +68,6 @@
|
||||||
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">{{ $t('common.aspectRatioLabel') }}</div>
|
|
||||||
<a-select
|
|
||||||
v-model="aspectRatio"
|
|
||||||
style="width: 100%">
|
|
||||||
<a-option value="auto">auto</a-option>
|
|
||||||
<a-option value="1:1">1:1</a-option>
|
|
||||||
<a-option value="1:4">1:4</a-option>
|
|
||||||
<a-option value="1:8">1:8</a-option>
|
|
||||||
<a-option value="2:3">2:3</a-option>
|
|
||||||
<a-option value="3:2">3:2</a-option>
|
|
||||||
<a-option value="3:4">3:4</a-option>
|
|
||||||
<a-option value="4:1">4:1</a-option>
|
|
||||||
<a-option value="4:3">4:3</a-option>
|
|
||||||
<a-option value="4:5">4:5</a-option>
|
|
||||||
<a-option value="5:4">5:4</a-option>
|
|
||||||
<a-option value="8:1">8:1</a-option>
|
|
||||||
<a-option value="9:16">9:16</a-option>
|
|
||||||
<a-option value="16:9">16:9</a-option>
|
|
||||||
<a-option value="21:9">21:9</a-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
<div class="nano-row">
|
|
||||||
<div class="nano-label">
|
|
||||||
{{ $t('common.resolutionLabel') }}
|
|
||||||
<span v-if="current === 2" class="required-star">*</span>
|
|
||||||
</div>
|
|
||||||
<a-select
|
|
||||||
v-model="resolution"
|
|
||||||
style="width: 100%">
|
|
||||||
<a-option value="1K">1K</a-option>
|
|
||||||
<a-option value="2K">2K</a-option>
|
|
||||||
<a-option value="4K">4K</a-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="nano-row"
|
|
||||||
v-if="current !== 2">
|
|
||||||
<div class="nano-label">输出格式</div>
|
|
||||||
<a-select
|
|
||||||
v-model="outputFormat"
|
|
||||||
style="width: 100%">
|
|
||||||
<a-option value="png">png</a-option>
|
|
||||||
<a-option value="jpg">jpg</a-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<mf-button
|
<mf-button
|
||||||
class="submit"
|
class="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -135,7 +85,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="right lab-canvas-bg"
|
class="right"
|
||||||
v-if="showResult">
|
v-if="showResult">
|
||||||
<div class="right-close">
|
<div class="right-close">
|
||||||
<mf-icon
|
<mf-icon
|
||||||
|
|
@ -143,16 +93,12 @@
|
||||||
@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 && isResultImageUrl">
|
v-show="imageUrl">
|
||||||
<mf-button @click="saveImage">
|
<mf-button @click="saveImage">
|
||||||
<a-image
|
<a-image
|
||||||
:width="20"
|
:width="20"
|
||||||
|
|
@ -161,6 +107,14 @@
|
||||||
src="/images/btn_bctp@2x.png" />
|
src="/images/btn_bctp@2x.png" />
|
||||||
{{ $t('common.saveImage') }}
|
{{ $t('common.saveImage') }}
|
||||||
</mf-button>
|
</mf-button>
|
||||||
|
<mf-button @click="jumpToVideo">
|
||||||
|
<a-image
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
:preview="false"
|
||||||
|
src="/images/btn_scsp@2x.png" />
|
||||||
|
{{ $t('common.generateVideo') }}
|
||||||
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -203,21 +157,16 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { nanoBananaImgToImg } from '@/api/ai'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
prefixCls: 'image',
|
prefixCls: 'image',
|
||||||
firstUrl: [],
|
firstUrl: '',
|
||||||
text: '',
|
text: '',
|
||||||
current: 1,
|
current: 1,
|
||||||
generateLoading: false,
|
generateLoading: false,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
taskIdHint: '',
|
|
||||||
aspectRatio: 'auto',
|
|
||||||
resolution: '1K',
|
|
||||||
outputFormat: 'png',
|
|
||||||
price: null,
|
price: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
selectedTags: {},
|
selectedTags: {},
|
||||||
|
|
@ -235,27 +184,12 @@ 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(to) {
|
handler(from, to) {
|
||||||
if (to.name === 'image-to-image2') {
|
let type = to?.query?.type || 1
|
||||||
this.current = 2
|
this.current = type
|
||||||
} 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
|
||||||
|
|
@ -265,12 +199,9 @@ 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 = Number(type) || 1
|
this.current = type || 1
|
||||||
}
|
}
|
||||||
let { text } = this.$route.query || {}
|
let { text } = this.$route.query || {}
|
||||||
if (text) {
|
if (text) {
|
||||||
|
|
@ -319,27 +250,11 @@ export default {
|
||||||
if (this.uploadType === 'upload') {
|
if (this.uploadType === 'upload') {
|
||||||
this.selectedTemplatePreview = ''
|
this.selectedTemplatePreview = ''
|
||||||
this.selectedTemplate = null
|
this.selectedTemplate = null
|
||||||
this.firstUrl = []
|
this.firstUrl = ''
|
||||||
} else {
|
} else {
|
||||||
this.firstUrl = []
|
this.firstUrl = ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getImageUrls() {
|
|
||||||
if (this.$datas.isEmpty(this.firstUrl)) return []
|
|
||||||
if (Array.isArray(this.firstUrl)) {
|
|
||||||
return this.firstUrl
|
|
||||||
.map((item) => item?.serverUrl || item?.url || '')
|
|
||||||
.filter((url) => !!url)
|
|
||||||
}
|
|
||||||
if (typeof this.firstUrl === 'object') {
|
|
||||||
const url = this.firstUrl.serverUrl || this.firstUrl.url
|
|
||||||
return url ? [url] : []
|
|
||||||
}
|
|
||||||
if (typeof this.firstUrl === 'string') {
|
|
||||||
return this.firstUrl ? [this.firstUrl] : []
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
// 打开模版选择弹窗
|
// 打开模版选择弹窗
|
||||||
openTemplateDialog() {
|
openTemplateDialog() {
|
||||||
this.templateDialogVisible = true
|
this.templateDialogVisible = true
|
||||||
|
|
@ -421,6 +336,9 @@ export default {
|
||||||
close() {
|
close() {
|
||||||
this.showResult = false
|
this.showResult = false
|
||||||
},
|
},
|
||||||
|
jumpToVideo() {
|
||||||
|
this.$router.push(`/fast-video?url=${this.imageUrl}`)
|
||||||
|
},
|
||||||
async saveImage() {
|
async saveImage() {
|
||||||
try {
|
try {
|
||||||
// 获取图片的 blob 数据
|
// 获取图片的 blob 数据
|
||||||
|
|
@ -447,10 +365,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generateImage() {
|
generateImage() {
|
||||||
const imageUrls = this.getImageUrls()
|
if (!this.firstUrl || !this.firstUrl.url) {
|
||||||
// image-to-image2 (current=2) 需要检查分辨率
|
this.$message.error(this.$t('common.uploadImageError'))
|
||||||
if (this.current === 2 && !this.resolution) {
|
|
||||||
this.$message.warning(this.$t('common.proResolutionRequired'))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if (!this.text && this.current == 2) {
|
// if (!this.text && this.current == 2) {
|
||||||
|
|
@ -476,34 +392,20 @@ export default {
|
||||||
tags.push(item.id)
|
tags.push(item.id)
|
||||||
})
|
})
|
||||||
this.generateLoading = true
|
this.generateLoading = true
|
||||||
this.taskIdHint = ''
|
this.$axios({
|
||||||
const requestData = {
|
url: 'api/ai/imgToImg',
|
||||||
functionType: this.current == 1 ? '11' : '12',
|
method: 'POST',
|
||||||
imageUrls,
|
data: {
|
||||||
text: this.text,
|
text: this.text,
|
||||||
tags: tags.join(','),
|
firstUrl: this.firstUrl.url,
|
||||||
aspectRatio: this.aspectRatio,
|
functionType: this.current == 1 ? '11' : '12',
|
||||||
resolution: this.resolution,
|
tags: tags.join(',')
|
||||||
numImages: 1
|
|
||||||
}
|
}
|
||||||
if (this.current !== 2) {
|
})
|
||||||
requestData.outputFormat = this.outputFormat
|
|
||||||
}
|
|
||||||
nanoBananaImgToImg(requestData)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.generateLoading = false
|
this.generateLoading = false
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
const payload = res.data !== undefined && res.data !== null ? res.data : res.msg
|
this.imageUrl = 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({
|
||||||
|
|
@ -549,27 +451,6 @@ 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;
|
||||||
|
|
@ -713,6 +594,7 @@ export default {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background-color: #000000;
|
||||||
padding: 0 100px 20px 100px;
|
padding: 0 100px 20px 100px;
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
|
|
@ -734,14 +616,6 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,18 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doSame(item) {
|
doSame(item) {
|
||||||
if (item.type == 1) {
|
if (item.type == 21) {
|
||||||
this.$router.push(`/image-to-image?text=${item.text}`)
|
this.$router.push(`/fast-video?text=${item.text}`)
|
||||||
|
} else if (item.type == 1) {
|
||||||
|
this.$router.push(`/fast-image?text=${item.text}`)
|
||||||
} else if (item.type == 11) {
|
} else if (item.type == 11) {
|
||||||
this.$router.push(`/image-to-image`) //?text=${item.text}
|
this.$router.push(`/image-to-image`) //?text=${item.text}
|
||||||
} else if (item.type == 12) {
|
} else if (item.type == 12) {
|
||||||
this.$router.push(`/image-to-image?type=2&text=${item.text}`)
|
this.$router.push(`/image-to-image?type=2&text=${item.text}`)
|
||||||
|
} else if (item.type == 13) {
|
||||||
|
this.$router.push(
|
||||||
|
`/change-face` //?text=${item.text}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelPreview() {
|
cancelPreview() {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,6 @@ export default {
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.auth-login {
|
.auth-login {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(180deg, #111111 0%, #0d0d0d 100%);
|
background: linear-gradient(0deg, #091125 0%, #000000 100%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default defineConfig({
|
||||||
less: {
|
less: {
|
||||||
modifyVars: {
|
modifyVars: {
|
||||||
'@size-9': '40px',
|
'@size-9': '40px',
|
||||||
'arcoblue-6': '#8899ff'
|
'arcoblue-6': '#e6217a'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,12 +226,6 @@
|
||||||
<artifactId>qiniu-java-sdk</artifactId>
|
<artifactId>qiniu-java-sdk</artifactId>
|
||||||
<version>${qiniu_api.version}</version>
|
<version>${qiniu_api.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- 腾讯云COS -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.qcloud</groupId>
|
|
||||||
<artifactId>cos_api</artifactId>
|
|
||||||
<version>5.6.155</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!--mybatis plus extension,包含了mybatis plus core-->
|
<!--mybatis plus extension,包含了mybatis plus core-->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@ 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;
|
||||||
import com.ruoyi.common.core.domain.model.LoginAiUser;
|
import com.ruoyi.common.core.domain.model.LoginAiUser;
|
||||||
import com.ruoyi.common.utils.TencentCosUtil;
|
import com.ruoyi.common.utils.AwsS3Util;
|
||||||
import com.ruoyi.common.utils.RandomStringUtil;
|
import com.ruoyi.common.utils.RandomStringUtil;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
|
@ -38,220 +36,13 @@ import java.util.regex.Pattern;
|
||||||
public class ByteApiController extends BaseController {
|
public class ByteApiController extends BaseController {
|
||||||
|
|
||||||
private final IByteService byteService;
|
private final IByteService byteService;
|
||||||
private final TencentCosUtil tencentCosUtil;
|
private final AwsS3Util awsS3Util;
|
||||||
private final IAiOrderService aiOrderService;
|
private final IAiOrderService aiOrderService;
|
||||||
private final IAiManagerService managerService;
|
private final IAiManagerService managerService;
|
||||||
private final IAiTagService aiTagService;
|
private final IAiTagService aiTagService;
|
||||||
@Value("${byteapi.callBackUrl}")
|
@Value("${byteapi.callBackUrl}")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@Value("${nanobanana.callbackUrl}")
|
|
||||||
private String nanoCallbackUrl;
|
|
||||||
|
|
||||||
@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());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
|
||||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
|
||||||
}
|
|
||||||
|
|
||||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(request.getNanoApiType(), nanoRequest);
|
|
||||||
|
|
||||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
|
||||||
aiOrderService.orderFailure(aiOrder);
|
|
||||||
return buildNanoGenerateFailureResult(nanoResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
String taskId = nanoResponse.getData().getTaskId();
|
|
||||||
aiOrder.setTaskId(taskId);
|
|
||||||
aiOrder.setResult(taskId);
|
|
||||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
|
||||||
aiOrderService.updateOrderTaskInfo(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 图生图:functionType=11调用v2,functionType=12调用pro")
|
|
||||||
@Transactional
|
|
||||||
public AjaxResult nanoImgToImg(@RequestBody NanoBananaPortalImgRequest request) {
|
|
||||||
logger.info("[NanoImgToImg] 前端请求入参: {}", request);
|
|
||||||
String functionType = request.getFunctionType();
|
|
||||||
if (StringUtils.isEmpty(functionType)) {
|
|
||||||
return AjaxResult.error("functionType is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据 functionType 确定调用的接口类型
|
|
||||||
String nanoApiType;
|
|
||||||
if ("11".equals(functionType)) {
|
|
||||||
nanoApiType = "v2"; // /image-to-image 调用 generate-2
|
|
||||||
} else if ("12".equals(functionType)) {
|
|
||||||
nanoApiType = "pro"; // /image-to-image2 调用 generate-pro
|
|
||||||
// pro 模式必须指定 resolution
|
|
||||||
if (StringUtils.isBlank(request.getResolution())) {
|
|
||||||
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return AjaxResult.error("无效的 functionType");
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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.isEmpty() ? null : 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());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
|
||||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("[NanoImgToImg] 上游请求参数: nanoApiType={}, requestBody={}", nanoApiType, nanoRequest);
|
|
||||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoApiType, nanoRequest);
|
|
||||||
logger.info("[NanoImgToImg] 上游响应结果: nanoApiType={}, response={}", nanoApiType, nanoResponse);
|
|
||||||
|
|
||||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
|
||||||
aiOrderService.orderFailure(aiOrder);
|
|
||||||
return buildNanoGenerateFailureResult(nanoResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
String taskId = nanoResponse.getData().getTaskId();
|
|
||||||
aiOrder.setTaskId(taskId);
|
|
||||||
aiOrder.setResult(taskId);
|
|
||||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
|
||||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
|
||||||
return AjaxResult.success(taskId);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
logger.error("[NanoImgToImg] 参数校验异常: functionType={}, nanoApiType={}, resolution={}, aspectRatio={}, numImages={}, imageUrlsSize={}, message={}",
|
|
||||||
functionType,
|
|
||||||
nanoApiType,
|
|
||||||
request.getResolution(),
|
|
||||||
request.getAspectRatio(),
|
|
||||||
request.getNumImages(),
|
|
||||||
imageUrls.size(),
|
|
||||||
e.getMessage(), e);
|
|
||||||
return AjaxResult.error(e.getMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("[NanoImgToImg] 调用上游异常并退款: functionType={}, nanoApiType={}, resolution={}, aspectRatio={}, numImages={}, imageUrlsSize={}, frontRequest={}, message={}",
|
|
||||||
functionType,
|
|
||||||
nanoApiType,
|
|
||||||
request.getResolution(),
|
|
||||||
request.getAspectRatio(),
|
|
||||||
request.getNumImages(),
|
|
||||||
imageUrls.size(),
|
|
||||||
request,
|
|
||||||
e.getMessage(), 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) {
|
||||||
|
|
@ -261,29 +52,31 @@ public class ByteApiController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
if (aiManager == null) {
|
String tags = request.getTags();
|
||||||
return AjaxResult.error("invalid functionType");
|
String text = "";
|
||||||
}
|
if (StringUtils.isNotEmpty(tags)) {
|
||||||
String text;
|
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
||||||
try {
|
List<String> tagPrompts = new ArrayList<>();
|
||||||
text = resolvePromptForPortal(request.getText(), request.getTags(), aiManager);
|
for (AiTag aiTag : aiTags) {
|
||||||
} catch (IllegalArgumentException e) {
|
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
||||||
if ("TAG_MISMATCH".equals(e.getMessage())) {
|
|
||||||
return AjaxResult.error(-3, "Generation failed, please try again");
|
return AjaxResult.error(-3, "Generation failed, please try again");
|
||||||
}
|
}
|
||||||
throw e;
|
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);
|
||||||
|
} 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 {
|
||||||
|
|
@ -291,49 +84,37 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||||
}
|
}
|
||||||
aiOrder.setText(text);
|
aiOrder.setText(text);
|
||||||
|
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
||||||
// 4. 组装NanoBanana (原火山)接口参数 - 符合用户需求
|
byteBodyReq.setModel("ep-20251104104536-2gpgz");
|
||||||
NanoBananaRequest nanoRequest = NanoBananaRequest.forTextToImage(
|
byteBodyReq.setPrompt(text);
|
||||||
text,
|
byteBodyReq.setSequential_image_generation("disabled");
|
||||||
nanoCallbackUrl,
|
byteBodyReq.setResponse_format("url");
|
||||||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
byteBodyReq.setSize("2K");
|
||||||
resolution
|
byteBodyReq.setStream(false);
|
||||||
);
|
byteBodyReq.setWatermark(false);
|
||||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq);
|
||||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
List<ByteDataRes> data = byteBodyRes.getData();
|
||||||
}
|
ByteDataRes byteDataRes = data.get(0);
|
||||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
String url = byteDataRes.getUrl();
|
||||||
|
url = awsS3Util.uploadFileByUrl(url);
|
||||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
if (url == null) {
|
||||||
logger.error("[PromptToImg] 生成失败并退款: functionType={}, nanoApiType={}, resolution={}, aspectRatio={}, textLen={}, upstreamCode={}, upstreamMsg={}, upstreamDataNull={}",
|
// 判断生成失败,退回金额逻辑
|
||||||
functionType,
|
|
||||||
nanoType,
|
|
||||||
resolution,
|
|
||||||
request.getAspectRatio(),
|
|
||||||
text == null ? 0 : text.length(),
|
|
||||||
nanoResponse == null ? null : nanoResponse.getCode(),
|
|
||||||
nanoResponse == null ? "NULL_RESPONSE" : nanoResponse.getMessage(),
|
|
||||||
nanoResponse == null || nanoResponse.getData() == null);
|
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
return buildNanoGenerateFailureResult(nanoResponse);
|
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
}
|
}
|
||||||
|
aiOrder.setResult(url);
|
||||||
String taskId = nanoResponse.getData().getTaskId();
|
aiOrderService.orderSuccess(aiOrder);
|
||||||
aiOrder.setTaskId(taskId); // 保存taskId用于回调匹配
|
return AjaxResult.success(url);
|
||||||
aiOrder.setResult(taskId); // 临时用taskId作为result,后续回调更新为真实URL
|
|
||||||
|
|
||||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
|
||||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
|
||||||
return AjaxResult.success(taskId); // 返回taskId给前端,后续可轮询或等待回调
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 判断生成失败,退回金额逻辑
|
// 判断生成失败,退回金额逻辑
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
// return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/imgToImg")
|
@PostMapping("/imgToImg")
|
||||||
@ApiOperation("图生图:functionType=11调用v2,functionType=12调用pro")
|
@ApiOperation("图生图")
|
||||||
@Transactional
|
@Transactional
|
||||||
public AjaxResult imgToImg(@RequestBody ByteApiRequest request) throws Exception {
|
public AjaxResult imgToImg(@RequestBody ByteApiRequest request) throws Exception {
|
||||||
String functionType = request.getFunctionType();
|
String functionType = request.getFunctionType();
|
||||||
|
|
@ -341,51 +122,37 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.error("functionType is null");
|
return AjaxResult.error("functionType is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 functionType 确定调用的接口类型
|
|
||||||
String nanoType;
|
|
||||||
if ("11".equals(functionType)) {
|
|
||||||
nanoType = "v2"; // functionType=11 调用 generate-2
|
|
||||||
} else if ("12".equals(functionType)) {
|
|
||||||
nanoType = "pro"; // functionType=12 调用 generate-pro
|
|
||||||
// pro 模式必须指定 resolution
|
|
||||||
if (StringUtils.isBlank(request.getResolution())) {
|
|
||||||
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return AjaxResult.error("无效的 functionType");
|
|
||||||
}
|
|
||||||
|
|
||||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
if (aiManager == null) {
|
String tags = request.getTags();
|
||||||
return AjaxResult.error("invalid functionType");
|
String text = "";
|
||||||
}
|
if (StringUtils.isNotEmpty(tags)) {
|
||||||
String text;
|
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
||||||
try {
|
List<String> tagPrompts = new ArrayList<>();
|
||||||
text = resolvePromptForPortal(request.getText(), request.getTags(), aiManager);
|
for (AiTag aiTag : aiTags) {
|
||||||
} catch (IllegalArgumentException e) {
|
if (!aiTag.getAiId().equals(aiManager.getId())) {
|
||||||
if ("TAG_MISMATCH".equals(e.getMessage())) {
|
|
||||||
return AjaxResult.error(-3, "Generation failed, please try again");
|
return AjaxResult.error(-3, "Generation failed, please try again");
|
||||||
}
|
}
|
||||||
throw e;
|
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);
|
||||||
|
} else {
|
||||||
|
text = aiManager.getPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(text)) {
|
if (StringUtils.isEmpty(text)) {
|
||||||
return AjaxResult.error("text is null");
|
return AjaxResult.error("text is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> imageUrls = new ArrayList<>();
|
Object firstUrl = request.getFirstUrl();
|
||||||
if (request.getImageUrls() != null && !request.getImageUrls().isEmpty()) {
|
if (null == firstUrl) {
|
||||||
for (String u : request.getImageUrls()) {
|
return AjaxResult.error("firstUrl is null");
|
||||||
if (StringUtils.isNotEmpty(u)) {
|
|
||||||
imageUrls.add(u.trim());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -393,32 +160,30 @@ public class ByteApiController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
aiOrder.setText(text);
|
aiOrder.setText(text);
|
||||||
aiOrder.setImg1(imageUrls.isEmpty() ? null : imageUrls.get(0));
|
|
||||||
|
|
||||||
NanoBananaRequest nanoRequest = NanoBananaRequest.forImageToImage(
|
aiOrder.setImg1(firstUrl.toString());
|
||||||
text,
|
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
||||||
imageUrls,
|
byteBodyReq.setModel("ep-20251104104536-2gpgz");
|
||||||
nanoCallbackUrl,
|
byteBodyReq.setPrompt(text);
|
||||||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
byteBodyReq.setImage(firstUrl);
|
||||||
resolution
|
byteBodyReq.setSequential_image_generation("disabled");
|
||||||
);
|
byteBodyReq.setResponse_format("url");
|
||||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
byteBodyReq.setSize("2K");
|
||||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
byteBodyReq.setStream(false);
|
||||||
}
|
byteBodyReq.setWatermark(false);
|
||||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq);
|
||||||
|
List<ByteDataRes> data = byteBodyRes.getData();
|
||||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
ByteDataRes byteDataRes = data.get(0);
|
||||||
|
String url = byteDataRes.getUrl();
|
||||||
|
url = awsS3Util.uploadFileByUrl(url);
|
||||||
|
if (url == null) {
|
||||||
|
// 判断生成失败,退回金额逻辑
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
return buildNanoGenerateFailureResult(nanoResponse);
|
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
}
|
}
|
||||||
|
aiOrder.setResult(url);
|
||||||
String taskId = nanoResponse.getData().getTaskId();
|
aiOrderService.orderSuccess(aiOrder);
|
||||||
aiOrder.setTaskId(taskId);
|
return AjaxResult.success(url);
|
||||||
aiOrder.setResult(taskId);
|
|
||||||
|
|
||||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
|
||||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
|
||||||
return AjaxResult.success(taskId);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
@ -534,7 +299,7 @@ public class ByteApiController extends BaseController {
|
||||||
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
||||||
content content = byteBodyRes.getContent();
|
content content = byteBodyRes.getContent();
|
||||||
String videoUrl = content.getVideo_url();
|
String videoUrl = content.getVideo_url();
|
||||||
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
|
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
|
||||||
content.setVideo_url(videoUrl);
|
content.setVideo_url(videoUrl);
|
||||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
||||||
AiOrder aiOrder = new AiOrder();
|
AiOrder aiOrder = new AiOrder();
|
||||||
|
|
@ -546,181 +311,23 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.success(byteBodyRes);
|
return AjaxResult.success(byteBodyRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@GetMapping(value = "/callBack")
|
||||||
* 解析提示词:优先 request.text;否则按标签替换模板;否则使用 AiManager 默认 prompt
|
@ApiOperation("视频下载回调")
|
||||||
*/
|
|
||||||
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生成回调(兼容 V2 和 Pro 格式)")
|
|
||||||
@Anonymous
|
@Anonymous
|
||||||
public AjaxResult nanoCallback(@RequestBody NanoBananaCallback callback) throws Exception {
|
public AjaxResult callBack(@PathVariable("id") ByteBodyRes byteBodyRes) throws Exception {
|
||||||
logger.info("[NanoCallback] 接收到回调请求: {}", callback);
|
|
||||||
if (callback == null || callback.getData() == null) {
|
|
||||||
logger.warn("[NanoCallback] 回调数据为空,callback={}", callback);
|
|
||||||
return buildNanoCallbackResult("EMPTY_DATA", AjaxResult.error("回调数据为空"));
|
|
||||||
}
|
|
||||||
|
|
||||||
NanoBananaCallback.NanoBananaCallbackData data = callback.getData();
|
|
||||||
String taskId = data.getTaskId();
|
|
||||||
Integer successFlag = data.getSuccessFlag();
|
|
||||||
logger.info("[NanoCallback] 关键字段解析: taskId={}, successFlag={}", taskId, successFlag);
|
|
||||||
if (StringUtils.isBlank(taskId)) {
|
|
||||||
logger.warn("[NanoCallback] taskId为空,无法匹配订单,data={}", data);
|
|
||||||
return buildNanoCallbackResult("EMPTY_TASK_ID", AjaxResult.error("taskId为空"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 V2 或 Pro 格式中获取 resultImageUrl
|
|
||||||
String resultImageUrl = null;
|
|
||||||
try {
|
|
||||||
NanoBananaCallback.NanoBananaCallbackResponse response = data.getResponse();
|
|
||||||
NanoBananaCallback.NanoBananaCallbackInfo info = data.getInfo();
|
|
||||||
if (response != null) {
|
|
||||||
// V2 格式
|
|
||||||
resultImageUrl = response.getResultImageUrl();
|
|
||||||
logger.info("[NanoCallback] 命中 V2 响应结构, resultImageUrl={}", resultImageUrl);
|
|
||||||
} else if (info != null) {
|
|
||||||
// Pro 格式
|
|
||||||
resultImageUrl = info.getResultImageUrl();
|
|
||||||
logger.info("[NanoCallback] 命中 Pro 响应结构, resultImageUrl={}", resultImageUrl);
|
|
||||||
} else {
|
|
||||||
logger.warn("[NanoCallback] 未命中 V2/Pro 响应结构, data={}", data);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("[NanoCallback] 提取resultImageUrl异常: taskId={}, data={}", taskId, data, e);
|
|
||||||
return buildNanoCallbackResult("PARSE_RESULT_URL_ERROR", AjaxResult.error("回调结果解析异常"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据 successFlag 和 resultImageUrl 处理不同状态
|
|
||||||
// 某些 NanoBanana 回调场景 successFlag 可能为 null,但会返回有效 resultImageUrl,此时按成功处理。
|
|
||||||
boolean hasResultImage = StringUtils.isNotBlank(resultImageUrl);
|
|
||||||
boolean isSuccessByFlag = Integer.valueOf(1).equals(successFlag);
|
|
||||||
boolean isSuccessByCode = Integer.valueOf(200).equals(callback.getCode());
|
|
||||||
if (hasResultImage && (isSuccessByFlag || successFlag == null || isSuccessByCode)) {
|
|
||||||
// 成功 - 优先上传到腾讯云COS,若失败则回退使用回调原图URL写库
|
|
||||||
logger.info("[NanoCallback] 判定成功并开始上传结果图到COS: taskId={}, successFlag={}, originUrl={}",
|
|
||||||
taskId, successFlag, resultImageUrl);
|
|
||||||
String s3Url = null;
|
|
||||||
try {
|
|
||||||
s3Url = tencentCosUtil.uploadFileByUrl(resultImageUrl);
|
|
||||||
logger.info("[NanoCallback] COS上传结果: taskId={}, s3Url={}", taskId, s3Url);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 上游临时图可能有防盗链、过期或地域限制导致403,这里降级为直接使用回调原图URL写库
|
|
||||||
logger.error("[NanoCallback] COS上传异常,降级使用原图URL写库: taskId={}, originUrl={}",
|
|
||||||
taskId, resultImageUrl, e);
|
|
||||||
}
|
|
||||||
String finalResultUrl = StringUtils.isNotBlank(s3Url) ? s3Url : resultImageUrl;
|
|
||||||
if (StringUtils.isBlank(s3Url)) {
|
|
||||||
logger.warn("[NanoCallback] COS上传失败,回退使用回调原图URL写库: taskId={}, resultImageUrl={}",
|
|
||||||
taskId, resultImageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(taskId);
|
|
||||||
logger.info("[NanoCallback] 根据taskId查询订单结果: taskId={}, order={}", taskId, aiOrderByResult);
|
|
||||||
if (aiOrderByResult != null) {
|
|
||||||
aiOrderByResult.setResult(finalResultUrl);
|
|
||||||
aiOrderByResult.setTaskId(taskId);
|
|
||||||
// orderSuccess 会设置 status=1 并更新统计
|
|
||||||
aiOrderService.orderSuccess(aiOrderByResult);
|
|
||||||
logger.info("[NanoCallback] 回调处理成功并已更新订单: taskId={}, orderId={}, result={}",
|
|
||||||
taskId, aiOrderByResult.getId(), finalResultUrl);
|
|
||||||
return buildNanoCallbackResult("SUCCESS", AjaxResult.success("回调处理成功,图像已写入"));
|
|
||||||
}
|
|
||||||
logger.warn("[NanoCallback] 未找到对应订单,无法更新: taskId={}", taskId);
|
|
||||||
return buildNanoCallbackResult("ORDER_NOT_FOUND", AjaxResult.error("未找到对应订单"));
|
|
||||||
} else if (Integer.valueOf(0).equals(successFlag)) {
|
|
||||||
// 生成中 - 订单初始状态已经是 0,不需要额外更新
|
|
||||||
logger.info("[NanoCallback] 任务生成中: taskId={}", taskId);
|
|
||||||
return buildNanoCallbackResult("IN_PROGRESS", AjaxResult.success("任务生成中"));
|
|
||||||
} else if (Integer.valueOf(2).equals(successFlag) || Integer.valueOf(3).equals(successFlag)) {
|
|
||||||
// 失败 - 退款并更新订单状态为 2(失败)
|
|
||||||
logger.warn("[NanoCallback] 任务失败回调,准备退款: taskId={}, successFlag={}", taskId, successFlag);
|
|
||||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(taskId);
|
|
||||||
logger.info("[NanoCallback] 失败回调订单查询结果: taskId={}, order={}", taskId, aiOrderByResult);
|
|
||||||
if (aiOrderByResult != null) {
|
|
||||||
// orderFailure 会设置 status=2 并处理退款
|
|
||||||
aiOrderService.orderFailure(aiOrderByResult);
|
|
||||||
logger.info("[NanoCallback] 失败回调处理完成,已退款: taskId={}, orderId={}", taskId, aiOrderByResult.getId());
|
|
||||||
return buildNanoCallbackResult("FAILED_REFUNDED", AjaxResult.success("回调处理失败,已退款"));
|
|
||||||
}
|
|
||||||
logger.warn("[NanoCallback] 失败回调未找到订单,无法退款: taskId={}", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn("[NanoCallback] 未命中明确处理分支,按已接收返回: taskId={}, successFlag={}, resultImageUrl={}",
|
|
||||||
taskId, successFlag, resultImageUrl);
|
|
||||||
return buildNanoCallbackResult("FALLBACK", AjaxResult.success("回调已接收"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一打印 NanoBanana 回调返回结果,便于对比“接收内容-处理路径-返回值”。
|
|
||||||
*/
|
|
||||||
private AjaxResult buildNanoCallbackResult(String stage, AjaxResult result) {
|
|
||||||
logger.info("[NanoCallback] 返回结果: stage={}, code={}, msg={}",
|
|
||||||
stage, result.get("code"), result.get("msg"));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一处理 NanoBanana 提交任务失败场景,便于前端展示明确错误信息。
|
|
||||||
*/
|
|
||||||
private AjaxResult buildNanoGenerateFailureResult(NanoBananaResponse nanoResponse) {
|
|
||||||
if (nanoResponse != null && Integer.valueOf(402).equals(nanoResponse.getCode())) {
|
|
||||||
return AjaxResult.error(-4, "上游服务额度不足,请联系管理员充值");
|
|
||||||
}
|
|
||||||
if (nanoResponse != null
|
|
||||||
&& StringUtils.isNotBlank(nanoResponse.getMessage())
|
|
||||||
&& nanoResponse.getMessage().toLowerCase().contains("insufficient")) {
|
|
||||||
return AjaxResult.error(-4, "上游服务额度不足,请联系管理员充值");
|
|
||||||
}
|
|
||||||
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保留原有视频回调(GET转POST兼容)
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/callBack")
|
|
||||||
@ApiOperation("视频回调")
|
|
||||||
@Anonymous
|
|
||||||
public AjaxResult callBack(@RequestBody ByteBodyRes byteBodyRes) throws Exception {
|
|
||||||
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
||||||
String id = byteBodyRes.getId();
|
String id = byteBodyRes.getId();
|
||||||
content content = byteBodyRes.getContent();
|
content content = byteBodyRes.getContent();
|
||||||
String videoUrl = content.getVideo_url();
|
String videoUrl = content.getVideo_url();
|
||||||
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
|
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
|
||||||
content.setVideo_url(videoUrl);
|
content.setVideo_url(videoUrl);
|
||||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
||||||
if (aiOrderByResult != null) {
|
|
||||||
AiOrder aiOrder = new AiOrder();
|
AiOrder aiOrder = new AiOrder();
|
||||||
aiOrder.setId(aiOrderByResult.getId());
|
aiOrder.setId(aiOrderByResult.getId());
|
||||||
aiOrder.setResult(videoUrl);
|
aiOrder.setResult(videoUrl);
|
||||||
|
// aiOrder.setUpdateBy(SecurityUtils.getLoginAiUser().getUsername());
|
||||||
aiOrderService.updateAiOrder(aiOrder);
|
aiOrderService.updateAiOrder(aiOrder);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return AjaxResult.success(byteBodyRes);
|
return AjaxResult.success(byteBodyRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.ruoyi.api;
|
package com.ruoyi.api;
|
||||||
|
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.utils.TencentCosUtil;
|
import com.ruoyi.common.utils.AwsS3Util;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import io.swagger.annotations.ApiParam;
|
import io.swagger.annotations.ApiParam;
|
||||||
|
|
@ -18,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||||
@Api(tags = "文件上传")
|
@Api(tags = "文件上传")
|
||||||
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
public class FileController {
|
public class FileController {
|
||||||
private final TencentCosUtil tencentCosUtil;
|
private final AwsS3Util awsS3Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件上传
|
* 文件上传
|
||||||
|
|
@ -30,7 +30,7 @@ public class FileController {
|
||||||
@ApiParam(name = "file", value = "文件", required = true)
|
@ApiParam(name = "file", value = "文件", required = true)
|
||||||
@RequestParam("file") MultipartFile file) throws Exception {
|
@RequestParam("file") MultipartFile file) throws Exception {
|
||||||
AjaxResult ajax = AjaxResult.success();
|
AjaxResult ajax = AjaxResult.success();
|
||||||
String uploadUrl = tencentCosUtil.uploadMultipartFile(file);
|
String uploadUrl = awsS3Util.uploadMultipartFile(file, true);
|
||||||
ajax.put("url", uploadUrl);
|
ajax.put("url", uploadUrl);
|
||||||
return ajax;
|
return ajax;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,22 +31,6 @@ 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;
|
|
||||||
|
|
||||||
@ApiModelProperty(name = "图生图参考图列表,可为空")
|
|
||||||
private List<String> imageUrls;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NanoBanana 接口版本:v1(/generate)、v2(/generate-2)、pro(/generate-pro),默认 v2 兼容旧逻辑
|
|
||||||
*/
|
|
||||||
@ApiModelProperty(name = "NanoBanana API 类型: v1 | v2 | pro")
|
|
||||||
private String nanoApiType;
|
|
||||||
|
|
||||||
@ApiModelProperty(name = "输出格式:png / jpg")
|
|
||||||
private String outputFormat;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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(兼容字段,可为空)")
|
|
||||||
private String imageUrl;
|
|
||||||
|
|
||||||
@ApiModelProperty("多张参考图 URL(优先于 imageUrl,可为空数组)")
|
|
||||||
private List<String> imageUrls;
|
|
||||||
|
|
||||||
@ApiModelProperty("输出格式:png / jpg")
|
|
||||||
private String outputFormat;
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
@ApiModelProperty("输出格式:png / jpg")
|
|
||||||
private String outputFormat;
|
|
||||||
}
|
|
||||||
|
|
@ -203,11 +203,10 @@ google:
|
||||||
client-secret:
|
client-secret:
|
||||||
redirect-uri:
|
redirect-uri:
|
||||||
|
|
||||||
tencent:
|
tencentCos:
|
||||||
cos:
|
accessKey:
|
||||||
secretId:
|
|
||||||
secretKey:
|
secretKey:
|
||||||
region:
|
endpoint:
|
||||||
bucketName:
|
bucketName:
|
||||||
domain:
|
domain:
|
||||||
|
|
||||||
|
|
@ -223,14 +222,6 @@ byteapi:
|
||||||
apiKey: 327d2815-2516-44c2-9e32-2dc50bf7afd7
|
apiKey: 327d2815-2516-44c2-9e32-2dc50bf7afd7
|
||||||
callBackUrl: www.google.com
|
callBackUrl: www.google.com
|
||||||
|
|
||||||
nanobanana:
|
|
||||||
# NanoBanana API Token (Bearer Token)
|
|
||||||
token: 1e95b160056f4579a9949d2516f4a463
|
|
||||||
# 回调地址,需替换为实际部署域名 (POST接口)
|
|
||||||
callbackUrl: https://47.86.170.114:8013/api/ai/nano-callback
|
|
||||||
# NanoBanana API 基础地址(路径在代码中拼接)
|
|
||||||
apiUrl: https://api.nanobananaapi.ai
|
|
||||||
|
|
||||||
jinsha:
|
jinsha:
|
||||||
url: https://api.jinshapay.xyz
|
url: https://api.jinshapay.xyz
|
||||||
appId: 1763617360
|
appId: 1763617360
|
||||||
|
|
|
||||||
|
|
@ -3,211 +3,82 @@ package com.ruoyi.common.utils;
|
||||||
import com.qcloud.cos.COSClient;
|
import com.qcloud.cos.COSClient;
|
||||||
import com.qcloud.cos.ClientConfig;
|
import com.qcloud.cos.ClientConfig;
|
||||||
import com.qcloud.cos.auth.BasicCOSCredentials;
|
import com.qcloud.cos.auth.BasicCOSCredentials;
|
||||||
import com.qcloud.cos.auth.COSCredentials;
|
import com.qcloud.cos.http.HttpProtocol;
|
||||||
import com.qcloud.cos.model.ObjectMetadata;
|
import com.qcloud.cos.model.ObjectMetadata;
|
||||||
import com.qcloud.cos.model.PutObjectRequest;
|
import com.qcloud.cos.model.PutObjectRequest;
|
||||||
import com.qcloud.cos.model.PutObjectResult;
|
import com.qcloud.cos.model.PutObjectResult;
|
||||||
import com.qcloud.cos.region.Region;
|
import com.qcloud.cos.region.Region;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
|
||||||
* 腾讯云 COS 文件上传工具类
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
public class TencentCosUtil {
|
public class TencentCosUtil {
|
||||||
|
// 设置好账号的ACCESS_KEY和SECRET_KEY
|
||||||
// -------------------------- 配置参数 --------------------------
|
@Value("${tencentCos.accessKey}")
|
||||||
@Value("${tencent.cos.secretId}")
|
private String ACCESS_KEY;
|
||||||
private String SECRET_ID;
|
@Value("${tencentCos.secretKey}")
|
||||||
@Value("${tencent.cos.secretKey}")
|
|
||||||
private String SECRET_KEY;
|
private String SECRET_KEY;
|
||||||
@Value("${tencent.cos.region}")
|
// 要上传的空间(创建空间的名称)
|
||||||
private String REGION;
|
@Value("${tencentCos.bucketName}")
|
||||||
@Value("${tencent.cos.bucketName}")
|
private String bucketName;
|
||||||
private String BUCKET_NAME;
|
@Value("${tencentCos.endpoint}")
|
||||||
@Value("${tencent.cos.domain}")
|
private String endpoint;
|
||||||
|
// 使用的是测试域名
|
||||||
|
@Value("${tencentCos.domain}")
|
||||||
private String domain;
|
private String domain;
|
||||||
// ----------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取COS客户端(单例模式,避免重复创建连接)
|
|
||||||
*/
|
|
||||||
private COSClient getCosClient() {
|
|
||||||
COSCredentials cred = new BasicCOSCredentials(SECRET_ID, SECRET_KEY);
|
|
||||||
ClientConfig clientConfig = new ClientConfig(new Region(REGION));
|
|
||||||
return new COSClient(cred, clientConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
//文件上传
|
||||||
* 上传MultipartFile到COS,返回文件访问地址(兼容旧代码)
|
public String upload(MultipartFile file) {
|
||||||
*
|
|
||||||
* @param file 前端上传的MultipartFile
|
|
||||||
* @return 文件访问地址(URL字符串)
|
|
||||||
* @throws Exception 异常
|
|
||||||
*/
|
|
||||||
public String upload(MultipartFile file) throws Exception {
|
|
||||||
return uploadMultipartFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 3 生成 cos 客户端。
|
||||||
* 上传MultipartFile到COS,返回文件访问地址
|
COSClient cosClient = createCosClient();
|
||||||
*
|
|
||||||
* @param file 前端上传的MultipartFile
|
|
||||||
* @return 文件访问地址(URL字符串)
|
|
||||||
* @throws Exception 异常
|
|
||||||
*/
|
|
||||||
public String uploadMultipartFile(MultipartFile file) throws Exception {
|
|
||||||
if (file.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("上传文件不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path tempPath = convertToTempPath(file);
|
|
||||||
|
|
||||||
|
// 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式
|
||||||
|
// 对象键(Key)是对象在存储桶中的唯一标识。 998u-09iu-09i-333
|
||||||
|
//在文件名称前面添加uuid值
|
||||||
|
String key = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8) + "_"
|
||||||
|
+ file.getOriginalFilename();
|
||||||
|
//对上传文件分组,根据当前日期 /2022/11/11
|
||||||
|
String dateTime = new DateTime().toString("yyyy/MM/dd");
|
||||||
|
key = dateTime + "/" + key;
|
||||||
try {
|
try {
|
||||||
String cosKey = generateCosKey(file.getOriginalFilename());
|
//获取上传文件输入流
|
||||||
|
InputStream inputStream = file.getInputStream();
|
||||||
|
ObjectMetadata objectMetadata = new ObjectMetadata();
|
||||||
|
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||||
|
bucketName,
|
||||||
|
key,
|
||||||
|
inputStream,
|
||||||
|
objectMetadata);
|
||||||
|
// 高级接口会返回一个异步结果Upload
|
||||||
|
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
||||||
|
|
||||||
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, cosKey, tempPath.toFile());
|
//返回上传文件路径
|
||||||
PutObjectResult putObjectResult = getCosClient().putObject(putObjectRequest);
|
//https://ggkt-atguigu-1310644373.cos.ap-beijing.myqcloud.com/01.jpg
|
||||||
|
String url = domain + "/" + key;
|
||||||
return domain + cosKey;
|
return url;
|
||||||
} finally {
|
} catch (Exception e) {
|
||||||
Files.deleteIfExists(tempPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 MultipartFile 转换为临时文件的 Path(自动清理)
|
|
||||||
*/
|
|
||||||
public Path convertToTempPath(MultipartFile file) throws IOException {
|
|
||||||
if (file.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("上传文件不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
String suffix = originalFilename != null ?
|
|
||||||
originalFilename.substring(originalFilename.lastIndexOf(".")) : ".tmp";
|
|
||||||
|
|
||||||
Path tempFile = Files.createTempFile("temp_", suffix);
|
|
||||||
Files.write(tempFile, file.getBytes());
|
|
||||||
tempFile.toFile().deleteOnExit();
|
|
||||||
|
|
||||||
return tempFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String uploadFileByUrl(String fileUrl) throws Exception {
|
|
||||||
return uploadFileByUrl(fileUrl, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过下载链接上传文件到COS
|
|
||||||
public String uploadFileByUrl(String fileUrl, boolean isPublic) throws Exception {
|
|
||||||
if (fileUrl == null || fileUrl.trim().isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("文件下载链接不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path tempPath = downloadFileToTemp(fileUrl);
|
|
||||||
|
|
||||||
try {
|
|
||||||
String originalFileName = extractFileNameFromUrl(fileUrl);
|
|
||||||
String cosKey = generateCosKey(originalFileName);
|
|
||||||
|
|
||||||
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, cosKey, tempPath.toFile());
|
|
||||||
getCosClient().putObject(putObjectRequest);
|
|
||||||
|
|
||||||
return domain + cosKey;
|
|
||||||
} finally {
|
|
||||||
Files.deleteIfExists(tempPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助方法:生成COS文件键
|
|
||||||
private String generateCosKey(String originalFileName) {
|
|
||||||
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
|
|
||||||
String dateTime = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
|
|
||||||
return dateTime + "/" + uuid + "_" + originalFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助方法:通过链接下载文件到临时路径
|
|
||||||
private Path downloadFileToTemp(String fileUrl) throws Exception {
|
|
||||||
String suffix = getFileSuffixFromUrl(fileUrl);
|
|
||||||
Path tempPath = Files.createTempFile("url-upload-", suffix);
|
|
||||||
|
|
||||||
HttpURLConnection connection = null;
|
|
||||||
InputStream in = null;
|
|
||||||
OutputStream out = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
URL url = new URL(fileUrl);
|
|
||||||
connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod("GET");
|
|
||||||
connection.setConnectTimeout(5000);
|
|
||||||
connection.setReadTimeout(10000);
|
|
||||||
|
|
||||||
int responseCode = connection.getResponseCode();
|
|
||||||
if (responseCode < 200 || responseCode >= 300) {
|
|
||||||
throw new RuntimeException("文件下载失败,状态码:" + responseCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
in = connection.getInputStream();
|
|
||||||
out = Files.newOutputStream(tempPath);
|
|
||||||
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (out != null) {
|
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
if (in != null) {
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (connection != null) {
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tempPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助方法:从URL提取文件名
|
private COSClient createCosClient() {
|
||||||
private String extractFileNameFromUrl(String fileUrl) {
|
//1.1 初始化用户身份信息
|
||||||
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
|
BasicCOSCredentials credentials = new BasicCOSCredentials(ACCESS_KEY, SECRET_KEY);
|
||||||
if (fileName.contains("?")) {
|
//1.2 设置bucket的地域
|
||||||
fileName = fileName.split("\\?")[0];
|
Region region = new Region(endpoint);
|
||||||
|
ClientConfig clientConfig = new ClientConfig(region);
|
||||||
|
// 这里建议设置使用 https 协议
|
||||||
|
clientConfig.setHttpProtocol(HttpProtocol.https);
|
||||||
|
//1.3 生成cos客户端
|
||||||
|
return new COSClient(credentials, clientConfig);
|
||||||
}
|
}
|
||||||
return fileName.isEmpty() ? "default_file" : fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助方法:从URL提取文件后缀
|
|
||||||
private String getFileSuffixFromUrl(String fileUrl) {
|
|
||||||
String fileName = extractFileNameFromUrl(fileUrl);
|
|
||||||
if (fileName.contains(".")) {
|
|
||||||
return fileName.substring(fileName.lastIndexOf("."));
|
|
||||||
}
|
|
||||||
return ".tmp";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,6 @@ public class AiOrder extends BaseEntity {
|
||||||
/** 第二张图片 */
|
/** 第二张图片 */
|
||||||
private String img2;
|
private String img2;
|
||||||
|
|
||||||
/**
|
|
||||||
* NanoBanana任务ID (用于回调匹配)
|
|
||||||
*/
|
|
||||||
private String taskId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户UUID
|
* 用户UUID
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
package com.ruoyi.ai.domain;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NanoBanana API 回调请求体
|
|
||||||
* 符合阿里巴巴Java开发手册规范
|
|
||||||
*
|
|
||||||
* @author AI Assistant
|
|
||||||
* @date 2026-04-10
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class NanoBananaCallback {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 顶层响应码
|
|
||||||
*/
|
|
||||||
private Integer code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态消息
|
|
||||||
*/
|
|
||||||
private String msg;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回调数据
|
|
||||||
*/
|
|
||||||
private NanoBananaCallbackData data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class NanoBananaCallbackData {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务 ID
|
|
||||||
*/
|
|
||||||
@JsonProperty("taskId")
|
|
||||||
private String taskId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 本次调用使用的参数 JSON 字符串
|
|
||||||
*/
|
|
||||||
@JsonProperty("paramJson")
|
|
||||||
private String paramJson;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务完成时间
|
|
||||||
*/
|
|
||||||
@JsonProperty("completeTime")
|
|
||||||
private String completeTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应数据(V2 格式:包含图像URL)
|
|
||||||
*/
|
|
||||||
private NanoBananaCallbackResponse response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 信息数据(Pro 格式:包含图像URL)
|
|
||||||
*/
|
|
||||||
private NanoBananaCallbackInfo info;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成状态标志:
|
|
||||||
* 0-生成中, 1-成功, 2-创建失败, 3-生成失败
|
|
||||||
*/
|
|
||||||
@JsonProperty("successFlag")
|
|
||||||
private Integer successFlag;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误码(如 422 敏感内容)
|
|
||||||
*/
|
|
||||||
@JsonProperty("errorCode")
|
|
||||||
private Integer errorCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误消息
|
|
||||||
*/
|
|
||||||
@JsonProperty("errorMessage")
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作类型/模型标识
|
|
||||||
*/
|
|
||||||
@JsonProperty("operationType")
|
|
||||||
private String operationType;
|
|
||||||
|
|
||||||
@JsonProperty("createTime")
|
|
||||||
private String createTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class NanoBananaCallbackResponse {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 原始图像URL(可能为null)
|
|
||||||
*/
|
|
||||||
@JsonProperty("originImageUrl")
|
|
||||||
private String originImageUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 结果图像URL(成功时存在)
|
|
||||||
*/
|
|
||||||
@JsonProperty("resultImageUrl")
|
|
||||||
private String resultImageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class NanoBananaCallbackInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 结果图像URL(Pro 格式成功时存在)
|
|
||||||
*/
|
|
||||||
@JsonProperty("resultImageUrl")
|
|
||||||
private String resultImageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
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 文生图/图生图请求参数(内部组装,实际出站 JSON 由 ByteService 按 v1/v2/pro 分别构建)
|
|
||||||
*
|
|
||||||
* @author ruoyi
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
public class NanoBananaRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文本提示词,必填
|
|
||||||
*/
|
|
||||||
private String prompt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 参考图 URL(图生图)
|
|
||||||
*/
|
|
||||||
@JsonProperty("imageUrls")
|
|
||||||
private List<String> imageUrls;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成类型(/generate 必填)
|
|
||||||
* TEXTTOIAMGE / IMAGETOIAMGE
|
|
||||||
*/
|
|
||||||
private String type = "TEXTTOIAMGE";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 宽高比:v1/v2 对应官方 image_size;pro 对应 aspectRatio
|
|
||||||
*/
|
|
||||||
private String imageSize = "auto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分辨率(主要用于 pro)
|
|
||||||
*/
|
|
||||||
private String resolution = "1K";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成张数 1-4(/generate)
|
|
||||||
*/
|
|
||||||
private Integer numImages = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输出格式:png / jpg
|
|
||||||
*/
|
|
||||||
private String outputFormat;
|
|
||||||
|
|
||||||
@JsonProperty("callBackUrl")
|
|
||||||
private String callBackUrl;
|
|
||||||
|
|
||||||
public static NanoBananaRequest forTextToImage(String prompt, String callBackUrl, String imageSize, String resolution) {
|
|
||||||
NanoBananaRequest req = new NanoBananaRequest();
|
|
||||||
req.setPrompt(prompt);
|
|
||||||
req.setCallBackUrl(callBackUrl);
|
|
||||||
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 imageSize, String resolution) {
|
|
||||||
NanoBananaRequest req = forTextToImage(prompt, callBackUrl, imageSize, resolution);
|
|
||||||
req.setType("IMAGETOIAMGE");
|
|
||||||
if (imageUrls != null && !imageUrls.isEmpty()) {
|
|
||||||
req.setImageUrls(imageUrls);
|
|
||||||
}
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package com.ruoyi.ai.domain;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NanoBanana API 提交任务响应
|
|
||||||
* 符合阿里巴巴Java开发手册规范
|
|
||||||
*
|
|
||||||
* @author AI Assistant
|
|
||||||
* @date 2026-04-10
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class NanoBananaResponse {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应码,200 表示成功
|
|
||||||
*/
|
|
||||||
private Integer code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应消息(部分接口字段名为 msg)
|
|
||||||
*/
|
|
||||||
@JsonProperty("message")
|
|
||||||
@JsonAlias("msg")
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应数据
|
|
||||||
*/
|
|
||||||
private NanoBananaData data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class NanoBananaData {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务ID,用于后续回调匹配
|
|
||||||
*/
|
|
||||||
@JsonProperty("taskId")
|
|
||||||
private String taskId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -80,9 +80,4 @@ public interface IAiOrderService {
|
||||||
AiOrder getAiOrderByResult(String result);
|
AiOrder getAiOrderByResult(String result);
|
||||||
|
|
||||||
BigDecimal getSumAmountByUserId(String userId);
|
BigDecimal getSumAmountByUserId(String userId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新订单任务信息(提交任务时调用)
|
|
||||||
*/
|
|
||||||
void updateOrderTaskInfo(AiOrder aiOrder);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,57 +2,26 @@ package com.ruoyi.ai.service;
|
||||||
|
|
||||||
import com.ruoyi.ai.domain.ByteBodyReq;
|
import com.ruoyi.ai.domain.ByteBodyReq;
|
||||||
import com.ruoyi.ai.domain.ByteBodyRes;
|
import com.ruoyi.ai.domain.ByteBodyRes;
|
||||||
import com.ruoyi.ai.domain.NanoBananaRequest;
|
|
||||||
import com.ruoyi.ai.domain.NanoBananaResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ByteService 接口
|
|
||||||
* 现已切换为 NanoBanana API(原Byte/火山引擎接口已弃用)
|
|
||||||
* 符合阿里巴巴Java开发手册规范
|
|
||||||
*
|
|
||||||
* @author shi
|
|
||||||
* @date 2026-04-10
|
|
||||||
*/
|
|
||||||
public interface IByteService {
|
public interface IByteService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文生图 - 使用 NanoBanana API 异步生成,返回 taskId(默认走 v2)
|
* 文生图
|
||||||
*/
|
*/
|
||||||
NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图生图 - 使用 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 替代
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
ByteBodyRes promptToImg(ByteBodyReq req) throws Exception;
|
ByteBodyRes promptToImg(ByteBodyReq req) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 旧接口兼容(图生图)
|
* 图生图
|
||||||
* @deprecated 使用 generateImageWithReference 替代
|
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
|
||||||
ByteBodyRes imgToImg(ByteBodyReq req) throws Exception;
|
ByteBodyRes imgToImg(ByteBodyReq req) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首尾帧图生视频(保持原有)
|
* 首尾帧图生视频
|
||||||
*/
|
*/
|
||||||
ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception;
|
ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载视频(保持原有)
|
* 下载视频
|
||||||
*/
|
*/
|
||||||
ByteBodyRes uploadVideo(String id) throws Exception;
|
ByteBodyRes uploadVideo(String id) throws Exception;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,15 +196,6 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
||||||
aiStatisticsService.saveOrUpdateData(aiStatistics);
|
aiStatisticsService.saveOrUpdateData(aiStatistics);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新订单任务信息(提交任务时调用,不更新统计)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void updateOrderTaskInfo(AiOrder aiOrder) {
|
|
||||||
aiOrderMapper.updateById(aiOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AiOrder getAiOrderByResult(String result) {
|
public AiOrder getAiOrderByResult(String result) {
|
||||||
LambdaQueryWrapper<AiOrder> query = Wrappers.lambdaQuery();
|
LambdaQueryWrapper<AiOrder> query = Wrappers.lambdaQuery();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ package com.ruoyi.ai.service.impl;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.ruoyi.ai.domain.*;
|
import com.ruoyi.ai.domain.ByteBodyReq;
|
||||||
|
import com.ruoyi.ai.domain.ByteBodyRes;
|
||||||
import com.ruoyi.ai.service.IByteService;
|
import com.ruoyi.ai.service.IByteService;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.http.OkHttpUtils;
|
import com.ruoyi.common.utils.http.OkHttpUtils;
|
||||||
|
|
@ -11,39 +12,18 @@ 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.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ByteService 实现类
|
|
||||||
* NanoBanana API 配置优化:基础地址 https://api.nanobananaapi.ai + 路径在代码中拼接
|
|
||||||
* 原火山/Byte接口保留用于视频功能
|
|
||||||
* 符合阿里巴巴Java开发手册规范,使用清晰注释、异常处理
|
|
||||||
*
|
|
||||||
* @author shi
|
|
||||||
* @date 2026-04-23
|
|
||||||
*/
|
|
||||||
@Service
|
@Service
|
||||||
public class ByteService implements IByteService {
|
public class ByteService implements IByteService {
|
||||||
|
|
||||||
|
// private final OkHttpClient okHttpClient = OkHttpUtils.createOkHttpClient();
|
||||||
|
|
||||||
// Jackson用于JSON序列化/反序列化
|
// Jackson用于JSON序列化/反序列化
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
.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地址(可配置在配置文件中)
|
||||||
private String nanoApiUrl;
|
|
||||||
|
|
||||||
@Value("${nanobanana.token:}")
|
|
||||||
private String nanoToken;
|
|
||||||
|
|
||||||
@Value("${nanobanana.callbackUrl:}")
|
|
||||||
private String callbackUrl;
|
|
||||||
|
|
||||||
// 保留原有Byte配置用于视频功能
|
|
||||||
@Value("${byteapi.url}")
|
@Value("${byteapi.url}")
|
||||||
private String API_URL;
|
private String API_URL;
|
||||||
|
|
||||||
|
|
@ -51,168 +31,60 @@ public class ByteService implements IByteService {
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception {
|
public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception {
|
||||||
return generateNanoBanana("v2", req);
|
return this.imgToImg(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception {
|
public ByteBodyRes imgToImg(ByteBodyReq req) throws Exception {
|
||||||
return generateNanoBanana("v2", req);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// 1. 验证请求参数(可选,根据业务需求)
|
||||||
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("imgToImg error:prompt is null");
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(req.getCallBackUrl())) {
|
|
||||||
req.setCallBackUrl(callbackUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String mode = nanoApiType == null ? "v2" : nanoApiType.trim().toLowerCase(Locale.ROOT);
|
// 2. 构建请求体JSON(基于ByteBodyReq的字段)
|
||||||
if ("pro".equals(mode)) {
|
// 注意:ByteBodyReq需包含与API参数对应的字段(model、prompt等)
|
||||||
validateAndNormalizeProResolution(req);
|
String jsonBody = objectMapper.writeValueAsString(req);
|
||||||
}
|
// 3. 构建请求
|
||||||
|
|
||||||
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(fullApiUrl)
|
.url(API_URL + "/images/generations")
|
||||||
.header("Authorization", "Bearer " + nanoToken)
|
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
|
// .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.1.1", 8080)))
|
||||||
.post(RequestBody.create(
|
.post(RequestBody.create(
|
||||||
MediaType.parse("application/json; charset=utf-8"),
|
MediaType.parse("application/json"),
|
||||||
jsonBody
|
jsonBody
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
// 4. 发送同步请求(因方法需要返回值,使用execute而非enqueue)
|
||||||
Response response = OkHttpUtils.newCall(request).execute();
|
Response response = OkHttpUtils.newCall(request).execute();
|
||||||
|
// 5. 处理响应
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
String errorMsg = response.body() != null ? response.body().string() : "NanoBanana error";
|
// 非200状态:返回错误信息(假设ByteBodyRes有error字段)
|
||||||
throw new Exception("NanoBanana API调用失败: " + errorMsg);
|
String errorMsg = response.body() != null ? response.body().string() : "imgToImg error";
|
||||||
|
throw new Exception("imgToImg error:" + errorMsg);
|
||||||
}
|
}
|
||||||
|
// 6. 解析成功响应为ByteBodyRes
|
||||||
if (response.body() == null) {
|
if (response.body() == null) {
|
||||||
throw new Exception("NanoBanana API响应为空");
|
throw new Exception("imgToImg response null");
|
||||||
}
|
}
|
||||||
|
|
||||||
String responseBody = response.body().string();
|
String responseBody = response.body().string();
|
||||||
return objectMapper.readValue(responseBody, NanoBananaResponse.class);
|
return objectMapper.readValue(responseBody, ByteBodyRes.class);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 官方 /generate 与 /generate-2 共用字段:prompt、type、callBackUrl、image_size、numImages、imageUrls、outputFormat
|
|
||||||
*/
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(req.getOutputFormat())) {
|
|
||||||
m.put("outputFormat", req.getOutputFormat());
|
|
||||||
}
|
|
||||||
return objectMapper.writeValueAsString(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 官方 /generate-pro:prompt、callBackUrl、aspectRatio、resolution、imageUrls、outputFormat
|
|
||||||
*/
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(req.getOutputFormat())) {
|
|
||||||
m.put("outputFormat", req.getOutputFormat());
|
|
||||||
}
|
|
||||||
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
|
|
||||||
@Deprecated
|
|
||||||
public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception {
|
|
||||||
// 兼容旧代码,实际已切换到NanoBanana
|
|
||||||
throw new UnsupportedOperationException("promptToImg 已弃用,请使用 generateImage");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
public ByteBodyRes imgToImg(ByteBodyReq req) throws Exception {
|
|
||||||
throw new UnsupportedOperationException("imgToImg 已弃用,请使用 generateImageWithReference");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception {
|
public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception {
|
||||||
// 视频功能保持原有Byte实现
|
|
||||||
if (StringUtils.isBlank(req.getPrompt())) {
|
|
||||||
throw new Exception("imgToVideo error:prompt is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 1. 验证请求参数(可选,根据业务需求)
|
||||||
|
// if (StringUtils.isBlank(req.getPrompt())) {
|
||||||
|
// throw new Exception("imgToVideo error:prompt is null");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 2. 构建请求体JSON(基于ByteBodyReq的字段)
|
||||||
|
// 注意:ByteBodyReq需包含与API参数对应的字段(model、prompt等)
|
||||||
String jsonBody = objectMapper.writeValueAsString(req);
|
String jsonBody = objectMapper.writeValueAsString(req);
|
||||||
|
// 3. 构建请求
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(API_URL + "/contents/generations/tasks")
|
.url(API_URL + "/contents/generations/tasks")
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
|
|
@ -222,47 +94,51 @@ public class ByteService implements IByteService {
|
||||||
jsonBody
|
jsonBody
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
// 4. 发送同步请求(因方法需要返回值,使用execute而非enqueue)
|
||||||
Response response = OkHttpUtils.newCall(request).execute();
|
Response response = OkHttpUtils.newCall(request).execute();
|
||||||
|
// 5. 处理响应
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
|
// 非200状态:返回错误信息(假设ByteBodyRes有error字段)
|
||||||
String errorMsg = response.body() != null ? response.body().string() : "imgToVideo error";
|
String errorMsg = response.body() != null ? response.body().string() : "imgToVideo error";
|
||||||
throw new Exception("imgToVideo error:" + errorMsg);
|
throw new Exception("imgToVideo error:" + errorMsg);
|
||||||
}
|
}
|
||||||
|
// 6. 解析成功响应为ByteBodyRes
|
||||||
if (response.body() == null) {
|
if (response.body() == null) {
|
||||||
throw new Exception("imgToVideo response null");
|
throw new Exception("imgToVideo response null");
|
||||||
}
|
}
|
||||||
|
|
||||||
String responseBody = response.body().string();
|
String responseBody = response.body().string();
|
||||||
return objectMapper.readValue(responseBody, ByteBodyRes.class);
|
return objectMapper.readValue(responseBody, ByteBodyRes.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBodyRes uploadVideo(String id) throws Exception {
|
public ByteBodyRes uploadVideo(String id) throws Exception {
|
||||||
// 视频功能保持原有Byte实现
|
// 1. 验证请求参数(可选,根据业务需求)
|
||||||
if (StringUtils.isBlank(id)) {
|
if (StringUtils.isBlank(id)) {
|
||||||
throw new Exception("uploadVideo error:id is null");
|
throw new Exception("uploadVideo error:id is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 构建请求体JSON(基于ByteBodyReq的字段)
|
||||||
|
// 注意:ByteBodyReq需包含与API参数对应的字段(model、prompt等)
|
||||||
|
//String jsonBody = objectMapper.writeValueAsString(req);
|
||||||
|
// 3. 构建请求
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(API_URL + "/contents/generations/tasks/" + id)
|
.url(API_URL + "/contents/generations/tasks/" + id)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Authorization", "Bearer " + apiKey)
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
.get()
|
.get()
|
||||||
.build();
|
.build();
|
||||||
|
// 4. 发送同步请求(因方法需要返回值,使用execute而非enqueue)
|
||||||
Response response = OkHttpUtils.newCall(request).execute();
|
Response response = OkHttpUtils.newCall(request).execute();
|
||||||
|
// 5. 处理响应
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
|
// 非200状态:返回错误信息(假设ByteBodyRes有error字段)
|
||||||
String errorMsg = response.body() != null ? response.body().string() : "uploadVideo error";
|
String errorMsg = response.body() != null ? response.body().string() : "uploadVideo error";
|
||||||
throw new Exception("uploadVideo error:" + errorMsg);
|
throw new Exception("uploadVideo error:" + errorMsg);
|
||||||
}
|
}
|
||||||
|
// 6. 解析成功响应为ByteBodyRes
|
||||||
if (response.body() == null) {
|
if (response.body() == null) {
|
||||||
throw new Exception("uploadVideo response null");
|
throw new Exception("uploadVideo response null");
|
||||||
}
|
}
|
||||||
|
|
||||||
String responseBody = response.body().string();
|
String responseBody = response.body().string();
|
||||||
return objectMapper.readValue(responseBody, ByteBodyRes.class);
|
return objectMapper.readValue(responseBody, ByteBodyRes.class);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
-- NanoBanana 异步任务:订单表需存储 taskId 供回调按任务匹配
|
|
||||||
-- 若库中已存在该列,执行会报错,可先执行:SHOW COLUMNS FROM ai_order LIKE 'task_id';
|
|
||||||
|
|
||||||
ALTER TABLE `ai_order`
|
|
||||||
ADD COLUMN `task_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'NanoBanana任务ID(回调匹配)' AFTER `img2`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,202 +0,0 @@
|
||||||
-- NanoBanana 标签测试数据(幂等脚本)
|
|
||||||
-- 适用场景:portal-ui 快捷生图(functionType=1)
|
|
||||||
-- 执行方式:在目标库执行本脚本,可重复执行,不会产生重复标签
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
|
|
||||||
-- 1) 获取快捷生图 ai_id(优先 type=1,兜底标题=快捷生图)
|
|
||||||
SET @ai_id := (
|
|
||||||
SELECT id
|
|
||||||
FROM ai_manager
|
|
||||||
WHERE del_flag = '0'
|
|
||||||
AND (type = '1' OR title = '快捷生图')
|
|
||||||
ORDER BY CASE WHEN type = '1' THEN 0 ELSE 1 END, id
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 如果没有查到 ai_id,下面语句会因为 @ai_id 为 NULL 不插入数据
|
|
||||||
-- 可先检查:SELECT @ai_id;
|
|
||||||
|
|
||||||
-- 2) 创建父级标签(与 prompt 占位符一一对应)
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '发型', 0, NULL, '髮型', 'hairstyle'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '发型');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '眼睛颜色', 0, NULL, '眼睛顏色', 'eye color'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '眼睛颜色');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '配饰', 0, NULL, '配飾', 'accessory'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '配饰');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '表情', 0, NULL, '表情', 'expression'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '表情');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '姿势', 0, NULL, '姿勢', 'pose'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '姿势');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '背景', 0, NULL, '背景', 'background'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '背景');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '服装描述', 0, NULL, '服裝描述', 'outfit'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '服装描述');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '胸部大小', 0, NULL, '胸部大小', 'breast size'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '胸部大小');
|
|
||||||
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), 0, @ai_id, '多人描述', 0, NULL, '多人描述', 'multi person'
|
|
||||||
FROM dual
|
|
||||||
WHERE @ai_id IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '多人描述');
|
|
||||||
|
|
||||||
-- 3) 读取父级标签 ID
|
|
||||||
SET @p_hair := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '发型' LIMIT 1);
|
|
||||||
SET @p_eye := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '眼睛颜色' LIMIT 1);
|
|
||||||
SET @p_acc := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '配饰' LIMIT 1);
|
|
||||||
SET @p_exp := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '表情' LIMIT 1);
|
|
||||||
SET @p_pose := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '姿势' LIMIT 1);
|
|
||||||
SET @p_bg := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '背景' LIMIT 1);
|
|
||||||
SET @p_outfit := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '服装描述' LIMIT 1);
|
|
||||||
SET @p_breast := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '胸部大小' LIMIT 1);
|
|
||||||
SET @p_multi := (SELECT id FROM ai_tag WHERE ai_id = @ai_id AND parent_id = 0 AND title = '多人描述' LIMIT 1);
|
|
||||||
|
|
||||||
-- 4) 子标签(prompt 字段建议使用英文短语,避免模板替换后的语言混杂)
|
|
||||||
-- 发型
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), @p_hair, @ai_id, '黑长直', 0, 'long straight black hair', '黑長直', 'long straight black hair'
|
|
||||||
FROM dual WHERE @p_hair IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = @p_hair AND title = '黑长直');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), @p_hair, @ai_id, '浅金波浪长发', 0, 'long wavy blonde hair', '淺金波浪長髮', 'long wavy blonde hair'
|
|
||||||
FROM dual WHERE @p_hair IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = @p_hair AND title = '浅金波浪长发');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), @p_hair, @ai_id, '银白短发', 0, 'short silver hair', '銀白短髮', 'short silver hair'
|
|
||||||
FROM dual WHERE @p_hair IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = @p_hair AND title = '银白短发');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US)
|
|
||||||
SELECT '0', 'seed', NOW(), 'seed', NOW(), @p_hair, @ai_id, '双马尾', 0, 'twin tails hairstyle', '雙馬尾', 'twin tails hairstyle'
|
|
||||||
FROM dual WHERE @p_hair IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id = @ai_id AND parent_id = @p_hair AND title = '双马尾');
|
|
||||||
|
|
||||||
-- 眼睛颜色
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_eye,@ai_id,'琥珀色',0,'amber eyes','琥珀色','amber eyes' FROM dual
|
|
||||||
WHERE @p_eye IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_eye AND title='琥珀色');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_eye,@ai_id,'蓝色',0,'deep blue eyes','藍色','deep blue eyes' FROM dual
|
|
||||||
WHERE @p_eye IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_eye AND title='蓝色');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_eye,@ai_id,'绿色',0,'emerald green eyes','綠色','emerald green eyes' FROM dual
|
|
||||||
WHERE @p_eye IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_eye AND title='绿色');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_eye,@ai_id,'紫色',0,'violet eyes','紫色','violet eyes' FROM dual
|
|
||||||
WHERE @p_eye IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_eye AND title='紫色');
|
|
||||||
|
|
||||||
-- 配饰
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_acc,@ai_id,'无配饰',0,'no accessories','無配飾','no accessories' FROM dual
|
|
||||||
WHERE @p_acc IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_acc AND title='无配饰');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_acc,@ai_id,'银框眼镜',0,'silver frame glasses','銀框眼鏡','silver frame glasses' FROM dual
|
|
||||||
WHERE @p_acc IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_acc AND title='银框眼镜');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_acc,@ai_id,'珍珠耳环',0,'pearl earrings','珍珠耳環','pearl earrings' FROM dual
|
|
||||||
WHERE @p_acc IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_acc AND title='珍珠耳环');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_acc,@ai_id,'发光项链',0,'glowing necklace','發光項鍊','glowing necklace' FROM dual
|
|
||||||
WHERE @p_acc IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_acc AND title='发光项链');
|
|
||||||
|
|
||||||
-- 表情
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_exp,@ai_id,'微笑',0,'gentle smile','微笑','gentle smile' FROM dual
|
|
||||||
WHERE @p_exp IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_exp AND title='微笑');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_exp,@ai_id,'冷艳',0,'cool confident expression','冷豔','cool confident expression' FROM dual
|
|
||||||
WHERE @p_exp IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_exp AND title='冷艳');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_exp,@ai_id,'害羞',0,'shy expression','害羞','shy expression' FROM dual
|
|
||||||
WHERE @p_exp IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_exp AND title='害羞');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_exp,@ai_id,'俏皮眨眼',0,'playful wink','俏皮眨眼','playful wink' FROM dual
|
|
||||||
WHERE @p_exp IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_exp AND title='俏皮眨眼');
|
|
||||||
|
|
||||||
-- 姿势
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_pose,@ai_id,'站立回眸',0,'standing and looking back','站立回眸','standing and looking back' FROM dual
|
|
||||||
WHERE @p_pose IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_pose AND title='站立回眸');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_pose,@ai_id,'双手叉腰',0,'standing hands on hips','雙手叉腰','standing hands on hips' FROM dual
|
|
||||||
WHERE @p_pose IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_pose AND title='双手叉腰');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_pose,@ai_id,'坐姿侧身',0,'sitting side pose','坐姿側身','sitting side pose' FROM dual
|
|
||||||
WHERE @p_pose IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_pose AND title='坐姿侧身');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_pose,@ai_id,'单手撩发',0,'one hand touching hair','單手撩髮','one hand touching hair' FROM dual
|
|
||||||
WHERE @p_pose IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_pose AND title='单手撩发');
|
|
||||||
|
|
||||||
-- 背景
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_bg,@ai_id,'海边日落',0,'sunset beach background','海邊日落','sunset beach background' FROM dual
|
|
||||||
WHERE @p_bg IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_bg AND title='海边日落');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_bg,@ai_id,'赛博都市夜景',0,'cyberpunk city at night','賽博都市夜景','cyberpunk city at night' FROM dual
|
|
||||||
WHERE @p_bg IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_bg AND title='赛博都市夜景');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_bg,@ai_id,'古典室内',0,'classic indoor background','古典室內','classic indoor background' FROM dual
|
|
||||||
WHERE @p_bg IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_bg AND title='古典室内');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_bg,@ai_id,'樱花街道',0,'sakura street background','櫻花街道','sakura street background' FROM dual
|
|
||||||
WHERE @p_bg IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_bg AND title='樱花街道');
|
|
||||||
|
|
||||||
-- 服装描述
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_outfit,@ai_id,'白色连衣裙',0,'white dress','白色連衣裙','white dress' FROM dual
|
|
||||||
WHERE @p_outfit IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_outfit AND title='白色连衣裙');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_outfit,@ai_id,'黑色皮衣',0,'black leather jacket outfit','黑色皮衣','black leather jacket outfit' FROM dual
|
|
||||||
WHERE @p_outfit IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_outfit AND title='黑色皮衣');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_outfit,@ai_id,'运动风套装',0,'sporty outfit','運動風套裝','sporty outfit' FROM dual
|
|
||||||
WHERE @p_outfit IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_outfit AND title='运动风套装');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_outfit,@ai_id,'旗袍',0,'elegant cheongsam','旗袍','elegant cheongsam' FROM dual
|
|
||||||
WHERE @p_outfit IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_outfit AND title='旗袍');
|
|
||||||
|
|
||||||
-- 胸部大小
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_breast,@ai_id,'小',0,'small bust','小','small bust' FROM dual
|
|
||||||
WHERE @p_breast IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_breast AND title='小');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_breast,@ai_id,'中',0,'medium bust','中','medium bust' FROM dual
|
|
||||||
WHERE @p_breast IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_breast AND title='中');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_breast,@ai_id,'大',0,'large bust','大','large bust' FROM dual
|
|
||||||
WHERE @p_breast IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_breast AND title='大');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_breast,@ai_id,'超大',0,'very large bust','超大','very large bust' FROM dual
|
|
||||||
WHERE @p_breast IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_breast AND title='超大');
|
|
||||||
|
|
||||||
-- 多人描述
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_multi,@ai_id,'单人',0,'single person only','單人','single person only' FROM dual
|
|
||||||
WHERE @p_multi IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_multi AND title='单人');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_multi,@ai_id,'双人同框',0,'two persons in frame','雙人同框','two persons in frame' FROM dual
|
|
||||||
WHERE @p_multi IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_multi AND title='双人同框');
|
|
||||||
INSERT INTO ai_tag (del_flag, create_by, create_time, update_by, update_time, parent_id, ai_id, title, status, prompt, title_HK, title_US) SELECT '0','seed',NOW(),'seed',NOW(),@p_multi,@ai_id,'三人构图',0,'three persons composition','三人構圖','three persons composition' FROM dual
|
|
||||||
WHERE @p_multi IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ai_tag WHERE ai_id=@ai_id AND parent_id=@p_multi AND title='三人构图');
|
|
||||||
|
|
||||||
-- 5) 同步图生图(type=11)模版,避免与测试标签占位符不一致
|
|
||||||
UPDATE ai_manager
|
|
||||||
SET title = '图生图',
|
|
||||||
status = 1,
|
|
||||||
prompt = '1girl, solo, exact same character as reference image, (completely nude:1.2), naked, no clothes, breasts fully exposed, {胸部大小}, perfect anatomy, masterpiece, best quality',
|
|
||||||
parent_id_sort = NULL
|
|
||||||
WHERE type = '11'
|
|
||||||
AND del_flag = '0';
|
|
||||||
|
|
||||||
-- 6) 对齐快捷生图(type=1) parentIdSort(按当前 prompt 的占位符顺序)
|
|
||||||
UPDATE ai_manager
|
|
||||||
SET parent_id_sort = CONCAT_WS(',',
|
|
||||||
@p_hair, @p_eye, @p_acc, @p_exp, @p_pose, @p_bg, @p_outfit, @p_breast, @p_multi
|
|
||||||
)
|
|
||||||
WHERE id = @ai_id
|
|
||||||
AND @ai_id IS NOT NULL;
|
|
||||||
|
|
||||||
-- 7) 快速校验
|
|
||||||
-- SELECT id, parent_id, ai_id, title, prompt FROM ai_tag WHERE ai_id = @ai_id ORDER BY parent_id, id;
|
|
||||||
-- SELECT id, title, type, parent_id_sort FROM ai_manager WHERE id = @ai_id;
|
|
||||||
Loading…
Reference in New Issue