fix: nanobanana 优化
This commit is contained in:
parent
04ede952ac
commit
a8d789bdc9
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
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代码。**
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
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调用必须添加中文注释说明用途。
|
||||
|
||||
**所有前端与后端交互的代码必须严格遵守以上规则。**
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
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 完全一致的视觉风格。**
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
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` 做回归
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
######################################################################
|
||||
# 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,7 +8,12 @@
|
|||
<link
|
||||
rel="icon"
|
||||
href="/images/logo.png" />
|
||||
<title>asio</title>
|
||||
<title>纳绘香蕉</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>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export default {
|
|||
name: 'App',
|
||||
created() {
|
||||
this.$auth.toggleTheme()
|
||||
document.body.setAttribute('arco-theme', 'dark')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -17,31 +17,33 @@ body {
|
|||
body {
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #1f2329;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: rgba(232, 236, 245, 0.92);
|
||||
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #0d0d0d;
|
||||
}
|
||||
|
||||
// firefox 滚动条样式
|
||||
* {
|
||||
scrollbar-color: rgba(144, 147, 153, 0.5) #0f0f12;
|
||||
scrollbar-color: rgba(136, 153, 255, 0.45) #141418;
|
||||
}
|
||||
|
||||
/*滚动条整体样式*/
|
||||
::-webkit-scrollbar {
|
||||
background-color: #0f0f12;
|
||||
background-color: #141418;
|
||||
}
|
||||
|
||||
/*滚动条里面小方块*/
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: red;
|
||||
border: 3px solid #0f0f12;
|
||||
background: linear-gradient(180deg, rgba(120, 170, 255, 0.55), rgba(160, 130, 255, 0.5));
|
||||
border: 2px solid #141418;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/*滚动条里面轨道*/
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0f0f12;
|
||||
background: #141418;
|
||||
}
|
||||
|
||||
:focus {
|
||||
|
|
@ -240,14 +242,17 @@ input:-webkit-autofill {
|
|||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
z-index: 100 !important;
|
||||
background: #fff;
|
||||
background: #111111;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.arco-modal-simple {
|
||||
background: #0f0f12;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #e6217a;
|
||||
background: rgba(20, 20, 20, 0.92);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
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 {
|
||||
color: #fff;
|
||||
}
|
||||
|
|
@ -273,216 +278,248 @@ input:-webkit-autofill {
|
|||
}
|
||||
}
|
||||
|
||||
/* ====================== 新粗野主义风格 (New Brutalism) ====================== */
|
||||
/* ====================== 暗黑实验室风 (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: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #2c2c2c;
|
||||
font-family: 'Impact', 'Arial Black', 'Helvetica Neue', 'Microsoft YaHei', system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.025em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f0f0f0;
|
||||
/* 粗糙肌理感:模拟混凝土/纸张纹理,使用细网格噪声 */
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
background: #0d0d0d;
|
||||
background-image:
|
||||
linear-gradient(45deg, rgba(44, 44, 44, 0.015) 1px, transparent 1px),
|
||||
linear-gradient(-45deg, rgba(44, 44, 44, 0.01) 1px, transparent 1px);
|
||||
background-size: 5px 5px;
|
||||
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: 'Impact', 'Arial Black', sans-serif;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 0.95;
|
||||
margin: 0 0 4px 0;
|
||||
text-transform: uppercase;
|
||||
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);
|
||||
}
|
||||
|
||||
/* 所有主要元素:硬边、无圆角、4px硬阴影、2px边框 */
|
||||
.brutalist,
|
||||
.arco-card,
|
||||
.arco-btn,
|
||||
.arco-menu,
|
||||
.arco-modal-content,
|
||||
.arco-drawer-content,
|
||||
.input-wrapper,
|
||||
.arco-input-wrapper,
|
||||
.arco-select,
|
||||
.arco-textarea,
|
||||
.arco-input,
|
||||
select,
|
||||
textarea,
|
||||
input,
|
||||
.mf-button,
|
||||
.mf-pane,
|
||||
.card,
|
||||
.panel,
|
||||
.navbar,
|
||||
.sidebar-container,
|
||||
.app-wrapper {
|
||||
border: 2px solid #2c2c2c !important;
|
||||
/* 生成画布: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;
|
||||
box-shadow: 4px 4px 0 #2c2c2c !important;
|
||||
background-color: #f0f0f0 !important;
|
||||
color: #2c2c2c !important;
|
||||
transition: all 0.1s linear !important;
|
||||
}
|
||||
|
||||
.brutalist:active,
|
||||
.arco-btn:active,
|
||||
.arco-menu-item:active,
|
||||
.mf-button:active {
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 2px 2px 0 #2c2c2c !important;
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 主按钮:深灰底 + 鲜红强调,冲突感 */
|
||||
.arco-btn,
|
||||
.mf-button {
|
||||
background: #2c2c2c !important;
|
||||
color: #f0f0f0 !important;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 12px 24px !important;
|
||||
font-size: 15px;
|
||||
border: 2px solid #2c2c2c !important;
|
||||
box-shadow: 4px 4px 0 #2c2c2c !important;
|
||||
.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;
|
||||
}
|
||||
|
||||
.arco-btn:hover,
|
||||
.arco-btn-primary,
|
||||
.mf-button:hover,
|
||||
.mf-button[type="primary"] {
|
||||
background: #ff0033 !important;
|
||||
color: #f0f0f0 !important;
|
||||
border-color: #2c2c2c !important;
|
||||
box-shadow: 6px 6px 0 #2c2c2c !important;
|
||||
transform: translate(-1px, -1px);
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 菜单 & 侧边 */
|
||||
.arco-menu,
|
||||
.sidebar-container {
|
||||
background: #e8e8e8 !important;
|
||||
border-right: 4px solid #2c2c2c !important;
|
||||
.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);
|
||||
}
|
||||
|
||||
.arco-menu-item,
|
||||
.sidebar-container :deep(.arco-menu-item) {
|
||||
border: 2px solid #2c2c2c !important;
|
||||
.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: 16px 20px !important;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
background: #f0f0f0 !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;
|
||||
}
|
||||
|
||||
.arco-menu-item:hover,
|
||||
.arco-menu-item.arco-menu-selected,
|
||||
.sidebar-container :deep(.arco-menu-item:hover),
|
||||
.sidebar-container :deep(.arco-menu-selected) {
|
||||
background: #ff0033 !important;
|
||||
color: #f0f0f0 !important;
|
||||
border-color: #2c2c2c;
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 模态 & 弹窗 - 更厚重 */
|
||||
.arco-modal-content,
|
||||
.arco-drawer-content,
|
||||
.arco-modal-simple {
|
||||
border: 4px solid #2c2c2c !important;
|
||||
box-shadow: 8px 8px 0 #2c2c2c !important;
|
||||
background: #f0f0f0 !important;
|
||||
.dark-lab .arco-modal-content,
|
||||
.dark-lab .arco-drawer-content {
|
||||
.lab-glass();
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.arco-modal-title,
|
||||
.arco-modal-header {
|
||||
background: #2c2c2c !important;
|
||||
color: #f0f0f0 !important;
|
||||
border-bottom: 4px solid #ff0033 !important;
|
||||
padding: 16px !important;
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 导航栏特定 */
|
||||
.navbar {
|
||||
background: #f0f0f0 !important;
|
||||
border-bottom: 4px solid #2c2c2c !important;
|
||||
box-shadow: 0 4px 0 #2c2c2c !important;
|
||||
padding: 0 20px !important;
|
||||
height: 72px !important; /* 略高以强调 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.right-menu-item.user {
|
||||
border: 2px solid #2c2c2c !important;
|
||||
background: #f0f0f0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
.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: -12px; /* 轻微重叠/非对称 */
|
||||
margin-right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 12px;
|
||||
padding: 20px 32px 20px 20px;
|
||||
background: #f0f0f0;
|
||||
border-left: 4px solid #ff0033;
|
||||
margin-left: 0;
|
||||
padding: 20px 28px;
|
||||
background: transparent;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
/* 高对比图片,无滤镜但增强 */
|
||||
img:not(.logo img),
|
||||
.arco-image img,
|
||||
.preview-image {
|
||||
filter: contrast(1.15) saturate(1.1);
|
||||
border: 3px solid #2c2c2c;
|
||||
box-shadow: 6px 6px 0 #2c2c2c;
|
||||
image-rendering: crisp-edges;
|
||||
/* 预览图:柔化边框,略浮起 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 滚动条适配粗野风格 - 硬边 */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid #2c2c2c;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2c2c2c;
|
||||
border: 2px solid #f0f0f0;
|
||||
box-shadow: 2px 2px 0 #ff0033;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #e0e0e0;
|
||||
border: 2px solid #2c2c2c;
|
||||
}
|
||||
|
||||
/* 强调冲突色 */
|
||||
.accent {
|
||||
color: #ff0033;
|
||||
font-weight: 900;
|
||||
color: #a8bfff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
border: 2px solid #2c2c2c;
|
||||
padding: 4px;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
.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,16 +1,17 @@
|
|||
body {
|
||||
// 新粗野主义风格 - 浅灰背景,硬边框
|
||||
--mf-color-bg-3: #f0f0f0;
|
||||
--his-border-color: #2c2c2c;
|
||||
--primary-6: 255 0 51; /* #ff0033 鲜红强调色 */
|
||||
--color-text-1: #2c2c2c;
|
||||
--color-text-2: #2c2c2c;
|
||||
--color-bg-1: #f0f0f0;
|
||||
--color-bg-2: #e0e0e0;
|
||||
/* 暗黑实验室:深底 + 霓虹蓝紫主色 */
|
||||
--mf-color-bg-3: #111111;
|
||||
--his-border-color: rgba(140, 180, 255, 0.28);
|
||||
--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'] {
|
||||
/* 保留暗黑支持,但优先粗野主义浅色 */
|
||||
--mf-color-bg-3: #2c2c2c;
|
||||
--primary-6: 255 0 51;
|
||||
--mf-color-bg-3: #141418;
|
||||
--primary-6: 136 153 255;
|
||||
--color-text-1: rgba(232, 236, 245, 0.94);
|
||||
--color-bg-1: #0d0d0d;
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<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 Card } from './Card.vue';
|
||||
export { default as PromptHighlightTextarea } from './PromptHighlightTextarea.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 RechargePc } from './RechargePc.vue';
|
||||
export { default as Preview } from './Preview.vue';
|
||||
|
|
|
|||
|
|
@ -42,10 +42,6 @@ export default {
|
|||
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.',
|
||||
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!',
|
||||
useLess: 'You cannot use this website...',
|
||||
loginAccount: 'Login Account',
|
||||
|
|
|
|||
|
|
@ -42,10 +42,6 @@ export default {
|
|||
rechartTip1: '使用錢包進行轉帳充值時,請務必確認轉帳錢包所屬的鏈網路!!!',
|
||||
rechartTip2: '充值入帳可能會有延遲,請稍等 3-5 分鐘後再刷新查詢',
|
||||
walletAddr: '錢包地址:',
|
||||
fbTitle: '警告!此網站僅適合成年人!',
|
||||
fbContent: '進入本網站即表示我確認已年滿 18 歲或以上',
|
||||
fbCancel: '我未滿 18',
|
||||
fbOK: '我已滿 18',
|
||||
sorry: '抱歉!',
|
||||
useLess: '您無法使用該網站...',
|
||||
loginAccount: '登入帳號',
|
||||
|
|
|
|||
|
|
@ -98,15 +98,11 @@ const openedKeys = computed(() => {
|
|||
})
|
||||
|
||||
const menuItemClick = (key) => {
|
||||
if (key == 'change-face-video') {
|
||||
$message.warning(generateLang('isDevelop'))
|
||||
}else {
|
||||
router.push({ name: key })
|
||||
if ($base.isMobile()) {
|
||||
store.dispatch('main/closeSideBar')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subMenuClick = (key, _openKeys) => {
|
||||
store.dispatch('main/setOpenedKeys', _openKeys)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@
|
|||
@cancel="userVisible = false"
|
||||
:visible="userVisible" />
|
||||
|
||||
<mf-forbidden :visible="showForbidden" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -136,7 +135,6 @@ export default {
|
|||
'theme',
|
||||
'permission_routes',
|
||||
'lang',
|
||||
'showForbidden',
|
||||
'showLogin',
|
||||
'sidebar'
|
||||
]),
|
||||
|
|
@ -249,9 +247,11 @@ export default {
|
|||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
height: 72px;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 4px solid #2c2c2c;
|
||||
box-shadow: 0 4px 0 #2c2c2c;
|
||||
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 {
|
||||
display: flex;
|
||||
|
|
@ -260,8 +260,8 @@ export default {
|
|||
|
||||
&-collapse {
|
||||
display: none;
|
||||
transition: 0.1s linear;
|
||||
color: #2c2c2c;
|
||||
transition: 0.2s ease;
|
||||
color: rgba(200, 215, 245, 0.9);
|
||||
font-size: 28px;
|
||||
|
||||
&.isCollapse {
|
||||
|
|
@ -279,19 +279,19 @@ export default {
|
|||
display: flex;
|
||||
align-items: flex-end;
|
||||
cursor: pointer;
|
||||
color: #2c2c2c;
|
||||
font-weight: 900;
|
||||
letter-spacing: -1px;
|
||||
font-size: 26px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(245, 247, 252, 0.96);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
font-size: 22px;
|
||||
|
||||
&-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border: 3px solid #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #ff0033;
|
||||
background: #f0f0f0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -310,9 +310,8 @@ export default {
|
|||
align-items: center;
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
|
@ -320,31 +319,30 @@ export default {
|
|||
|
||||
&.language {
|
||||
.mf-button {
|
||||
font-size: 15px;
|
||||
color: #2c2c2c;
|
||||
background-color: #f0f0f0;
|
||||
border: 2px solid #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
font-size: 14px;
|
||||
color: rgba(220, 228, 245, 0.92);
|
||||
background: rgba(28, 28, 32, 0.85);
|
||||
border: 1px solid rgba(140, 180, 255, 0.22);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 18px rgba(80, 120, 200, 0.08);
|
||||
&:hover {
|
||||
background-color: #ff0033;
|
||||
color: #f0f0f0;
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 2px 2px 0 #2c2c2c;
|
||||
background: rgba(136, 153, 255, 0.18);
|
||||
color: #fff;
|
||||
border-color: rgba(180, 200, 255, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.logout {
|
||||
.mf-button {
|
||||
background-color: #f0f0f0;
|
||||
border: 2px solid #2c2c2c;
|
||||
color: #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
background: rgba(28, 28, 32, 0.85);
|
||||
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 {
|
||||
background-color: #ff0033;
|
||||
color: #f0f0f0;
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 2px 2px 0 #2c2c2c;
|
||||
background: rgba(136, 153, 255, 0.18);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -352,12 +350,11 @@ export default {
|
|||
.login {
|
||||
width: 110px;
|
||||
cursor: pointer;
|
||||
/* 粗野主义按钮由全局base.less样式覆盖 */
|
||||
}
|
||||
|
||||
&.user {
|
||||
width: 170px;
|
||||
color: #2c2c2c;
|
||||
color: rgba(232, 236, 245, 0.92);
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -365,12 +362,11 @@ export default {
|
|||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 2px solid #2c2c2c;
|
||||
background: #f0f0f0;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
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 {
|
||||
display: flex;
|
||||
|
|
@ -378,7 +374,8 @@ export default {
|
|||
flex-grow: 1;
|
||||
.arco-image {
|
||||
margin-right: 8px;
|
||||
border: 1px solid #2c2c2c;
|
||||
border: 1px solid rgba(140, 180, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -387,39 +384,42 @@ export default {
|
|||
}
|
||||
|
||||
.nav-point-name {
|
||||
color: #2c2c2c;
|
||||
font-weight: 900;
|
||||
color: rgba(232, 236, 245, 0.92);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
width: 240px;
|
||||
margin-left: -8px;
|
||||
border: 2px solid #2c2c2c;
|
||||
background: #f0f0f0;
|
||||
box-shadow: 4px 4px 0 #2c2c2c;
|
||||
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 {
|
||||
.mf-divider {
|
||||
margin: 8px 0;
|
||||
border-color: #2c2c2c;
|
||||
border-color: rgba(140, 180, 255, 0.15);
|
||||
}
|
||||
.user-info-wrap {
|
||||
padding: 16px;
|
||||
border: 2px solid #2c2c2c;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid rgba(140, 180, 255, 0.12);
|
||||
background: rgba(22, 22, 28, 0.9);
|
||||
border-radius: 10px;
|
||||
|
||||
.mf-avatar {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid #2c2c2c;
|
||||
border: 1px solid rgba(140, 180, 255, 0.25);
|
||||
}
|
||||
|
||||
.user-info-name {
|
||||
width: 160px;
|
||||
font-weight: 900;
|
||||
color: #2c2c2c;
|
||||
font-weight: 600;
|
||||
color: rgba(245, 247, 252, 0.96);
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.5px;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
|
|
@ -427,17 +427,16 @@ export default {
|
|||
.user-info-service {
|
||||
width: 160px;
|
||||
font-size: 13px;
|
||||
color: #2c2c2c;
|
||||
font-weight: 700;
|
||||
color: rgba(200, 210, 230, 0.85);
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&:hover {
|
||||
background-color: #ff0033;
|
||||
color: #f0f0f0;
|
||||
background-color: rgba(136, 153, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
cursor: text;
|
||||
}
|
||||
|
|
@ -452,7 +451,7 @@ export default {
|
|||
&-collapse {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
color: #2c2c2c;
|
||||
color: rgba(200, 215, 245, 0.9);
|
||||
}
|
||||
|
||||
&-menu {
|
||||
|
|
|
|||
|
|
@ -110,11 +110,13 @@ export default {
|
|||
.sidebar-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: width 0.1s linear;
|
||||
transition: width 0.15s ease;
|
||||
z-index: 12;
|
||||
background-color: #f0f0f0;
|
||||
border-right: 4px solid #2c2c2c;
|
||||
box-shadow: 4px 0 0 #ff0033; /* 非对称强调 */
|
||||
background-color: rgba(14, 14, 18, 0.88);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border-right: 1px solid rgba(140, 180, 255, 0.15);
|
||||
box-shadow: 4px 0 28px rgba(0, 0, 0, 0.35);
|
||||
|
||||
&.collapsed {
|
||||
width: 56px !important;
|
||||
|
|
@ -126,34 +128,33 @@ export default {
|
|||
}
|
||||
|
||||
:deep(.arco-menu) {
|
||||
background-color: #f0f0f0;
|
||||
background-color: transparent;
|
||||
padding-left: 8px;
|
||||
border: 2px solid #2c2c2c;
|
||||
border: none;
|
||||
|
||||
&-item {
|
||||
width: 180px;
|
||||
background-color: #f0f0f0;
|
||||
color: #2c2c2c;
|
||||
border: 2px solid #2c2c2c;
|
||||
margin: 8px 4px;
|
||||
padding: 14px 18px;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
box-shadow: 3px 3px 0 #2c2c2c;
|
||||
transition: all 0.1s linear;
|
||||
background-color: transparent;
|
||||
color: rgba(200, 210, 230, 0.88);
|
||||
border: none;
|
||||
margin: 6px 4px;
|
||||
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,
|
||||
&:hover {
|
||||
background-color: #ff0033 !important;
|
||||
color: #f0f0f0 !important;
|
||||
border-color: #2c2c2c;
|
||||
transform: translate(3px, 3px);
|
||||
box-shadow: 1px 1px 0 #2c2c2c;
|
||||
background-color: rgba(136, 153, 255, 0.14) !important;
|
||||
color: #f0f4ff !important;
|
||||
transform: none;
|
||||
box-shadow: 0 0 22px rgba(120, 170, 255, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏旧的toggle,如果需要可重新启用 */
|
||||
.toogle-menu {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -167,7 +168,7 @@ export default {
|
|||
z-index: 10;
|
||||
height: calc(100% - 72px);
|
||||
overflow: hidden;
|
||||
border-right: 4px solid #2c2c2c;
|
||||
border-right: 1px solid rgba(140, 180, 255, 0.15);
|
||||
|
||||
&.collapsed {
|
||||
width: 0px !important;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="app-wrapper brutalist">
|
||||
<div class="app-wrapper dark-lab">
|
||||
<div class="fixed-header">
|
||||
<nav-bar class="nav-bar" />
|
||||
</div>
|
||||
|
|
@ -47,11 +47,11 @@ export default {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
background: #0d0d0d;
|
||||
|
||||
.fixed-header {
|
||||
z-index: 100;
|
||||
border-bottom: 4px solid #2c2c2c;
|
||||
border-bottom: 1px solid rgba(140, 180, 255, 0.15);
|
||||
|
||||
.navbar {
|
||||
height: 72px;
|
||||
|
|
@ -61,10 +61,9 @@ export default {
|
|||
.main-wrapper {
|
||||
display: flex;
|
||||
height: calc(100% - 72px);
|
||||
/* 非对称布局:侧边栏略微偏移,主内容有强调边框 */
|
||||
.sidebar-wrapper {
|
||||
border-right: 4px solid #ff0033;
|
||||
margin-right: -8px; /* 轻微非对称重叠感 */
|
||||
border-right: 1px solid rgba(140, 180, 255, 0.12);
|
||||
margin-right: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,53 +79,13 @@ export const constantRoutes = [{
|
|||
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: {
|
||||
title: 'fastVideo',
|
||||
menuItem: true,
|
||||
permission: "pass",
|
||||
icon: 'btn_kjsp'
|
||||
}
|
||||
}, {
|
||||
path: 'generated-images',
|
||||
name: 'generated-images',
|
||||
component: () => import('@/views/GeneratedImages.vue'),
|
||||
meta: {
|
||||
title: 'AI文生图',
|
||||
menuItem: true,
|
||||
menuItem: false,
|
||||
permission: "pass",
|
||||
icon: 'btn_kjst'
|
||||
}
|
||||
|
|
@ -207,36 +167,7 @@ function hasPermission({
|
|||
|
||||
// 权限控制
|
||||
router.beforeEach(async (to = {}, from, next) => {
|
||||
if (from.path != '/fast-video') {
|
||||
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
|
||||
|
|
@ -43,8 +43,6 @@ const getters = {
|
|||
messageData: (state) => state.main.messageData,
|
||||
// 未读消息数量
|
||||
messageCount: (state) => state.main.messageCount,
|
||||
// 是否显示18禁的弹窗
|
||||
showForbidden: state => state.main.showForbidden,
|
||||
// 是否阻止页面跳转
|
||||
showPrevent: state => state.main.showPrevent,
|
||||
showLogin: state => state.main.showLogin,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ const state = {
|
|||
topMenu: Cookies.get('topMenu'),
|
||||
// 当前语言
|
||||
language: Cookies.get('language') || 'en_US',
|
||||
// 主题 '':亮色 / dark:暗黑
|
||||
theme: $storage.get('theme') || '',
|
||||
// 主题 '':亮色 / dark:暗黑(默认暗黑实验室)
|
||||
theme: $storage.get('theme') || 'dark',
|
||||
// 系统端
|
||||
system: $storage.get('system') || '',
|
||||
// 当前系统的所有多语言数据
|
||||
|
|
@ -39,8 +39,6 @@ const state = {
|
|||
showMessage: false,
|
||||
messageData: {},
|
||||
messageCount: 0,
|
||||
// 是否显示18禁弹窗
|
||||
showForbidden: true,
|
||||
// 阻止页面切换
|
||||
showPrevent: false,
|
||||
showLogin: false
|
||||
|
|
@ -131,10 +129,6 @@ const mutations = {
|
|||
SET_UNREAD_MESSAGE(state, value) {
|
||||
state.messageCount = value;
|
||||
},
|
||||
SET_FORBIDDEN(state, value) {
|
||||
state.showForbidden = value;
|
||||
$storage.set('showForbidden', value)
|
||||
},
|
||||
SET_PREVENT(state, value) {
|
||||
state.showPrevent = value;
|
||||
},
|
||||
|
|
@ -254,12 +248,6 @@ const actions = {
|
|||
commit('SET_UNREAD_MESSAGE', parseInt(res))
|
||||
})
|
||||
},
|
||||
// 设置是否显示18禁弹窗
|
||||
setForbidden({
|
||||
commit
|
||||
}, value) {
|
||||
commit('SET_FORBIDDEN', value)
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="right"
|
||||
class="right lab-canvas-bg"
|
||||
v-if="showResult">
|
||||
<div class="right-close">
|
||||
<mf-icon
|
||||
|
|
@ -101,14 +101,6 @@
|
|||
src="/images/btn_bctp@2x.png" />
|
||||
{{ $t('common.saveImage') }}
|
||||
</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>
|
||||
|
|
@ -190,9 +182,6 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
jumpToVideo() {
|
||||
this.$router.push(`/fast-image?url=${this.imageUrl}`)
|
||||
},
|
||||
saveImage() {
|
||||
this.$file.downloadFile(this.imageUrl)
|
||||
},
|
||||
|
|
@ -380,7 +369,6 @@ export default {
|
|||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #000000;
|
||||
padding: 0 100px 20px 100px;
|
||||
|
||||
&-close {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="right"
|
||||
class="right lab-canvas-bg"
|
||||
v-if="showResult">
|
||||
<div class="right-close">
|
||||
<mf-icon
|
||||
|
|
@ -59,14 +59,6 @@
|
|||
src="/images/btn_bctp@2x.png" />
|
||||
{{ $t('common.saveImage') }}
|
||||
</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>
|
||||
|
|
@ -141,9 +133,6 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
jumpToVideo() {
|
||||
this.$router.push(`/fast-image?url=${this.imageUrl}`)
|
||||
},
|
||||
saveImage() {
|
||||
this.$file.downloadFile(this.imageUrl)
|
||||
},
|
||||
|
|
@ -286,7 +275,6 @@ export default {
|
|||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #000000;
|
||||
padding: 0 100px 20px 100px;
|
||||
|
||||
:deep(.arco-image-error) {
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@
|
|||
</div>
|
||||
</a-modal>
|
||||
<div
|
||||
class="right"
|
||||
class="right lab-canvas-bg"
|
||||
v-if="showResult">
|
||||
<div class="right-close">
|
||||
<mf-icon
|
||||
|
|
@ -711,7 +711,6 @@ export default {
|
|||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #000000;
|
||||
padding: 20px 100px 20px 100px;
|
||||
|
||||
:deep(.result-video) {
|
||||
|
|
|
|||
|
|
@ -1,369 +0,0 @@
|
|||
<template>
|
||||
<div class="generated-images-page">
|
||||
<div class="page-header">
|
||||
<div class="panel-title">
|
||||
<span>AI 文生图</span>
|
||||
<span class="subtitle">使用 NanoBanana 最新模型生成高质量图像</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="query-section">
|
||||
<a-form :model="formState" layout="vertical">
|
||||
<div class="form-grid">
|
||||
<!-- 功能类型选择 -->
|
||||
<div class="field">
|
||||
<label>功能类型</label>
|
||||
<a-select v-model:value="formState.functionType" style="width: 100%" @change="loadAiInfo">
|
||||
<a-select-option value="1">标准文生图</a-select-option>
|
||||
<a-select-option value="11">高级文生图</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- NanoBanana 接口版本 -->
|
||||
<div class="field">
|
||||
<label>NanoBanana 接口</label>
|
||||
<a-select v-model:value="formState.nanoApiType" style="width: 100%">
|
||||
<a-select-option value="v1">V1(官方 /generate)</a-select-option>
|
||||
<a-select-option value="v2">V2(/generate-2)</a-select-option>
|
||||
<a-select-option value="pro">Pro(/generate-pro)</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 提示词 -->
|
||||
<div class="field full-width">
|
||||
<label>描述提示词 <span class="required">*</span></label>
|
||||
<a-textarea
|
||||
v-model:value="formState.text"
|
||||
:rows="4"
|
||||
placeholder="描述你想要生成的图像,例如:a happy dog running in the park, highly detailed, 8k"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 宽高比 -->
|
||||
<div class="field">
|
||||
<label>宽高比</label>
|
||||
<a-select v-model:value="formState.aspectRatio" style="width: 100%">
|
||||
<a-select-option value="auto">自动 (auto)</a-select-option>
|
||||
<a-select-option value="1:1">1:1 正方形</a-select-option>
|
||||
<a-select-option value="16:9">16:9 横图</a-select-option>
|
||||
<a-select-option value="9:16">9:16 竖图</a-select-option>
|
||||
<a-select-option value="4:3">4:3</a-select-option>
|
||||
<a-select-option value="3:2">3:2</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 分辨率(Pro 必选 1K/2K/4K) -->
|
||||
<div class="field">
|
||||
<label>分辨率 <span v-if="formState.nanoApiType === 'pro'" class="required">*</span></label>
|
||||
<a-select v-model:value="formState.resolution" style="width: 100%">
|
||||
<a-select-option value="1K">1K (标准)</a-select-option>
|
||||
<a-select-option value="2K">2K (高清)</a-select-option>
|
||||
<a-select-option value="4K">4K (超清)</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="generateImage"
|
||||
style="min-width: 160px"
|
||||
>
|
||||
生成图像 (消耗 {{ price }} 余额)
|
||||
</a-button>
|
||||
<a-button @click="resetForm">重置</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示 -->
|
||||
<div class="result-section" v-if="resultImage">
|
||||
<div class="result-header">
|
||||
<span class="result-title">生成结果</span>
|
||||
<a-button type="link" @click="copyImageUrl">复制链接</a-button>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<a-image
|
||||
:src="resultImage"
|
||||
:preview="true"
|
||||
class="result-image"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-info">
|
||||
<p>任务ID: {{ taskId }}</p>
|
||||
<p v-if="status">状态: {{ status === 1 ? '生成成功' : '处理中...' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<div class="history-section" v-if="historyList.length > 0">
|
||||
<div class="panel-title">历史生成记录</div>
|
||||
<a-table
|
||||
:data-source="historyList"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
class="asset-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.result" width="80" :preview="true" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'orange'">
|
||||
{{ record.status === 1 ? '成功' : '生成中' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { nanoBananaGenerate, getAiManagerInfo } from '@/api/ai'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
const formState = reactive({
|
||||
functionType: '1',
|
||||
nanoApiType: 'v2',
|
||||
text: '',
|
||||
aspectRatio: 'auto',
|
||||
resolution: '1K',
|
||||
tags: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const price = ref(0)
|
||||
const resultImage = ref('')
|
||||
const taskId = ref('')
|
||||
const status = ref(0)
|
||||
const historyList = ref<any[]>([])
|
||||
|
||||
interface TableColumn {
|
||||
title: string
|
||||
dataIndex?: string
|
||||
key?: string
|
||||
width?: number
|
||||
ellipsis?: boolean
|
||||
}
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '提示词', dataIndex: 'text', ellipsis: true },
|
||||
{ title: '图像', key: 'image', width: 100 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '时间', dataIndex: 'createTime' }
|
||||
]
|
||||
|
||||
const loadAiInfo = async () => {
|
||||
try {
|
||||
const res = await getAiManagerInfo(formState.functionType)
|
||||
if (res.price) price.value = res.price
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const generateImageFn = async () => {
|
||||
if (!formState.text.trim()) {
|
||||
Message.warning('请输入提示词')
|
||||
return
|
||||
}
|
||||
if (formState.nanoApiType === 'pro' && !formState.resolution) {
|
||||
Message.warning('Pro 接口必须选择分辨率(1K / 2K / 4K)')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
functionType: formState.functionType,
|
||||
nanoApiType: formState.nanoApiType,
|
||||
text: formState.text,
|
||||
aspectRatio: formState.aspectRatio,
|
||||
resolution: formState.resolution,
|
||||
tags: formState.tags
|
||||
}
|
||||
|
||||
const res = await nanoBananaGenerate(params)
|
||||
|
||||
if (res.code === 200) {
|
||||
taskId.value = res.data || res.msg
|
||||
Message.success('任务已提交!任务ID: ' + taskId.value)
|
||||
resultImage.value = '' // 清空等待回调或轮询
|
||||
// 模拟轮询(实际生产建议使用WebSocket或定时查询订单)
|
||||
pollResult(taskId.value)
|
||||
} else {
|
||||
Message.error(res.msg || '生成失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
Message.error(error.message || '请求失败,请检查网络或Token配置')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const pollResult = (tid: string) => {
|
||||
let count = 0
|
||||
const timer = setInterval(async () => {
|
||||
count++
|
||||
if (count > 30) { // 最多轮询30次
|
||||
clearInterval(timer)
|
||||
Message.warning('生成时间较长,请稍后在「我的作品」中查看')
|
||||
return
|
||||
}
|
||||
// 这里实际应调用订单查询接口,暂时模拟成功
|
||||
if (count > 8) {
|
||||
clearInterval(timer)
|
||||
resultImage.value = 'https://images.iqyjsnwv.com/sample-' + Date.now() + '.jpg' // 演示图片
|
||||
status.value = 1
|
||||
historyList.value.unshift({
|
||||
id: Date.now(),
|
||||
text: formState.text.substring(0, 30) + '...',
|
||||
result: resultImage.value,
|
||||
status: 1,
|
||||
createTime: new Date().toLocaleString()
|
||||
})
|
||||
Message.success('图像生成成功!')
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formState.text = ''
|
||||
resultImage.value = ''
|
||||
taskId.value = ''
|
||||
status.value = 0
|
||||
}
|
||||
|
||||
const copyImageUrl = () => {
|
||||
if (resultImage.value) {
|
||||
navigator.clipboard.writeText(resultImage.value)
|
||||
Message.success('链接已复制')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAiInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.generated-images-page {
|
||||
padding: 32px;
|
||||
background: #f0f0f0;
|
||||
min-height: 100vh;
|
||||
color: #2c2c2c;
|
||||
/* 继承全局粗野主义:硬边、阴影、字体 */
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 4px solid #2c2c2c;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.panel-title {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -2px;
|
||||
text-transform: uppercase;
|
||||
color: #2c2c2c;
|
||||
.subtitle {
|
||||
font-size: 15px;
|
||||
color: #ff0033;
|
||||
margin-left: 16px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-section,
|
||||
.result-section,
|
||||
.history-section {
|
||||
background: #f0f0f0;
|
||||
border: 3px solid #2c2c2c;
|
||||
padding: 28px;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 6px 6px 0 #2c2c2c;
|
||||
/* 全局已覆盖圆角为0 */
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 900;
|
||||
color: #2c2c2c;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff0033;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.result-image {
|
||||
max-width: 100%;
|
||||
border: 4px solid #2c2c2c;
|
||||
box-shadow: 8px 8px 0 #2c2c2c;
|
||||
image-rendering: crisp-edges;
|
||||
/* 高对比 by global */
|
||||
}
|
||||
|
||||
.task-info {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: #e8e8e8;
|
||||
border: 2px solid #2c2c2c;
|
||||
box-shadow: 4px 4px 0 #ff0033;
|
||||
font-size: 14px;
|
||||
color: #2c2c2c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.asset-table {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.brutalist-card {
|
||||
border: 3px solid #2c2c2c;
|
||||
box-shadow: 6px 6px 0 #2c2c2c;
|
||||
}
|
||||
|
||||
/* 确保图片和按钮在视图中匹配 */
|
||||
.result-image,
|
||||
img {
|
||||
border: 4px solid #2c2c2c !important;
|
||||
box-shadow: 6px 6px 0 #2c2c2c !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.generated-images-page {
|
||||
padding: 20px;
|
||||
}
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -69,29 +69,30 @@
|
|||
:placeholder="$t('common.textPlaceholder')" />
|
||||
</div> -->
|
||||
<div class="nano-options">
|
||||
<div class="nano-row">
|
||||
<div class="nano-label">NanoBanana</div>
|
||||
<a-select v-model="nanoApiType" style="width: 100%">
|
||||
<a-select-option value="v1">V1(/generate)</a-select-option>
|
||||
<a-select-option value="v2">V2(/generate-2)</a-select-option>
|
||||
<a-select-option value="pro">Pro(/generate-pro)</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="nano-row">
|
||||
<div class="nano-label">{{ $t('common.aspectRatioLabel') }}</div>
|
||||
<a-select v-model="aspectRatio" style="width: 100%">
|
||||
<a-select-option value="auto">auto</a-select-option>
|
||||
<a-select-option value="1:1">1:1</a-select-option>
|
||||
<a-select-option value="16:9">16:9</a-select-option>
|
||||
<a-select-option value="9:16">9:16</a-select-option>
|
||||
<a-select-option value="4:3">4:3</a-select-option>
|
||||
<a-select-option value="1:4">1:4</a-select-option>
|
||||
<a-select-option value="1:8">1:8</a-select-option>
|
||||
<a-select-option value="2:3">2:3</a-select-option>
|
||||
<a-select-option value="3:2">3:2</a-select-option>
|
||||
<a-select-option value="3:4">3:4</a-select-option>
|
||||
<a-select-option value="4:1">4:1</a-select-option>
|
||||
<a-select-option value="4:3">4:3</a-select-option>
|
||||
<a-select-option value="4:5">4:5</a-select-option>
|
||||
<a-select-option value="5:4">5:4</a-select-option>
|
||||
<a-select-option value="8:1">8:1</a-select-option>
|
||||
<a-select-option value="9:16">9:16</a-select-option>
|
||||
<a-select-option value="16:9">16:9</a-select-option>
|
||||
<a-select-option value="21:9">21:9</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="nano-row">
|
||||
<div class="nano-label">
|
||||
{{ $t('common.resolutionLabel') }}
|
||||
<span v-if="nanoApiType === 'pro'" class="required-star">*</span>
|
||||
<span v-if="current === 2" class="required-star">*</span>
|
||||
</div>
|
||||
<a-select v-model="resolution" style="width: 100%">
|
||||
<a-select-option value="1K">1K</a-select-option>
|
||||
|
|
@ -99,6 +100,13 @@
|
|||
<a-select-option value="4K">4K</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="nano-row">
|
||||
<div class="nano-label">输出格式</div>
|
||||
<a-select v-model="outputFormat" style="width: 100%">
|
||||
<a-select-option value="png">png</a-select-option>
|
||||
<a-select-option value="jpg">jpg</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<mf-button
|
||||
class="submit"
|
||||
|
|
@ -117,7 +125,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="right"
|
||||
class="right lab-canvas-bg"
|
||||
v-if="showResult">
|
||||
<div class="right-close">
|
||||
<mf-icon
|
||||
|
|
@ -143,14 +151,6 @@
|
|||
src="/images/btn_bctp@2x.png" />
|
||||
{{ $t('common.saveImage') }}
|
||||
</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>
|
||||
|
||||
|
|
@ -205,9 +205,9 @@ export default {
|
|||
generateLoading: false,
|
||||
imageUrl: '',
|
||||
taskIdHint: '',
|
||||
nanoApiType: 'v2',
|
||||
aspectRatio: 'auto',
|
||||
resolution: '1K',
|
||||
outputFormat: 'png',
|
||||
price: null,
|
||||
tags: [],
|
||||
selectedTags: {},
|
||||
|
|
@ -395,9 +395,6 @@ export default {
|
|||
close() {
|
||||
this.showResult = false
|
||||
},
|
||||
jumpToVideo() {
|
||||
this.$router.push(`/fast-video?url=${this.imageUrl}`)
|
||||
},
|
||||
async saveImage() {
|
||||
try {
|
||||
// 获取图片的 blob 数据
|
||||
|
|
@ -428,7 +425,8 @@ export default {
|
|||
this.$message.error(this.$t('common.uploadImageError'))
|
||||
return
|
||||
}
|
||||
if (this.nanoApiType === 'pro' && !this.resolution) {
|
||||
// image-to-image2 (current=2) 需要检查分辨率
|
||||
if (this.current === 2 && !this.resolution) {
|
||||
this.$message.warning(this.$t('common.proResolutionRequired'))
|
||||
return
|
||||
}
|
||||
|
|
@ -458,12 +456,12 @@ export default {
|
|||
this.taskIdHint = ''
|
||||
nanoBananaImgToImg({
|
||||
functionType: this.current == 1 ? '11' : '12',
|
||||
nanoApiType: this.nanoApiType,
|
||||
imageUrl: this.firstUrl.url,
|
||||
text: this.text,
|
||||
tags: tags.join(','),
|
||||
aspectRatio: this.aspectRatio,
|
||||
resolution: this.resolution,
|
||||
outputFormat: this.outputFormat,
|
||||
numImages: 1
|
||||
})
|
||||
.then((res) => {
|
||||
|
|
@ -689,7 +687,6 @@ export default {
|
|||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #000000;
|
||||
padding: 0 100px 20px 100px;
|
||||
|
||||
&-close {
|
||||
|
|
|
|||
|
|
@ -116,18 +116,12 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
doSame(item) {
|
||||
if (item.type == 21) {
|
||||
this.$router.push(`/fast-video?text=${item.text}`)
|
||||
} else if (item.type == 1) {
|
||||
if (item.type == 1) {
|
||||
this.$router.push(`/fast-image?text=${item.text}`)
|
||||
} else if (item.type == 11) {
|
||||
this.$router.push(`/image-to-image`) //?text=${item.text}
|
||||
} else if (item.type == 12) {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default defineConfig({
|
|||
less: {
|
||||
modifyVars: {
|
||||
'@size-9': '40px',
|
||||
'arcoblue-6': '#e6217a'
|
||||
'arcoblue-6': '#8899ff'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,12 @@
|
|||
<artifactId>qiniu-java-sdk</artifactId>
|
||||
<version>${qiniu_api.version}</version>
|
||||
</dependency>
|
||||
<!-- 腾讯云COS -->
|
||||
<dependency>
|
||||
<groupId>com.qcloud</groupId>
|
||||
<artifactId>cos_api</artifactId>
|
||||
<version>5.6.155</version>
|
||||
</dependency>
|
||||
|
||||
<!--mybatis plus extension,包含了mybatis plus core-->
|
||||
<dependency>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.ruoyi.common.annotation.Anonymous;
|
|||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.model.LoginAiUser;
|
||||
import com.ruoyi.common.utils.AwsS3Util;
|
||||
import com.ruoyi.common.utils.TencentCosUtil;
|
||||
import com.ruoyi.common.utils.RandomStringUtil;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
|
|
@ -38,7 +38,7 @@ import java.util.regex.Pattern;
|
|||
public class ByteApiController extends BaseController {
|
||||
|
||||
private final IByteService byteService;
|
||||
private final AwsS3Util awsS3Util;
|
||||
private final TencentCosUtil tencentCosUtil;
|
||||
private final IAiOrderService aiOrderService;
|
||||
private final IAiManagerService managerService;
|
||||
private final IAiTagService aiTagService;
|
||||
|
|
@ -104,6 +104,9 @@ public class ByteApiController extends BaseController {
|
|||
if (request.getNumImages() != null) {
|
||||
nanoRequest.setNumImages(request.getNumImages());
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
||||
}
|
||||
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(request.getNanoApiType(), nanoRequest);
|
||||
|
||||
|
|
@ -115,7 +118,8 @@ public class ByteApiController extends BaseController {
|
|||
String taskId = nanoResponse.getData().getTaskId();
|
||||
aiOrder.setTaskId(taskId);
|
||||
aiOrder.setResult(taskId);
|
||||
aiOrderService.orderSuccess(aiOrder);
|
||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
||||
return AjaxResult.success(taskId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
|
|
@ -128,19 +132,27 @@ public class ByteApiController extends BaseController {
|
|||
}
|
||||
|
||||
@PostMapping("/nano/imgToImg")
|
||||
@ApiOperation("Portal 图生图:按 nanoApiType 调用 v1/v2/pro")
|
||||
@ApiOperation("Portal 图生图:functionType=11调用v2,functionType=12调用pro")
|
||||
@Transactional
|
||||
public AjaxResult nanoImgToImg(@RequestBody NanoBananaPortalImgRequest request) {
|
||||
String functionType = request.getFunctionType();
|
||||
if (StringUtils.isEmpty(functionType)) {
|
||||
return AjaxResult.error("functionType is null");
|
||||
}
|
||||
if (StringUtils.isEmpty(request.getNanoApiType())) {
|
||||
return AjaxResult.error("nanoApiType is null");
|
||||
}
|
||||
if ("pro".equalsIgnoreCase(request.getNanoApiType()) && StringUtils.isBlank(request.getResolution())) {
|
||||
|
||||
// 根据 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()) {
|
||||
|
|
@ -196,8 +208,11 @@ public class ByteApiController extends BaseController {
|
|||
if (request.getNumImages() != null) {
|
||||
nanoRequest.setNumImages(request.getNumImages());
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
||||
}
|
||||
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(request.getNanoApiType(), nanoRequest);
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoApiType, nanoRequest);
|
||||
|
||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
|
|
@ -207,7 +222,8 @@ public class ByteApiController extends BaseController {
|
|||
String taskId = nanoResponse.getData().getTaskId();
|
||||
aiOrder.setTaskId(taskId);
|
||||
aiOrder.setResult(taskId);
|
||||
aiOrderService.orderSuccess(aiOrder);
|
||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
||||
return AjaxResult.success(taskId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
|
|
@ -266,6 +282,9 @@ public class ByteApiController extends BaseController {
|
|||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||
resolution
|
||||
);
|
||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
||||
}
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
||||
|
||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||
|
|
@ -277,7 +296,8 @@ public class ByteApiController extends BaseController {
|
|||
aiOrder.setTaskId(taskId); // 保存taskId用于回调匹配
|
||||
aiOrder.setResult(taskId); // 临时用taskId作为result,后续回调更新为真实URL
|
||||
|
||||
aiOrderService.orderSuccess(aiOrder); // 先标记为进行中,回调成功后再更新
|
||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
||||
return AjaxResult.success(taskId); // 返回taskId给前端,后续可轮询或等待回调
|
||||
} catch (Exception e) {
|
||||
// 判断生成失败,退回金额逻辑
|
||||
|
|
@ -287,7 +307,7 @@ public class ByteApiController extends BaseController {
|
|||
}
|
||||
|
||||
@PostMapping("/imgToImg")
|
||||
@ApiOperation("图生图")
|
||||
@ApiOperation("图生图:functionType=11调用v2,functionType=12调用pro")
|
||||
@Transactional
|
||||
public AjaxResult imgToImg(@RequestBody ByteApiRequest request) throws Exception {
|
||||
String functionType = request.getFunctionType();
|
||||
|
|
@ -295,6 +315,20 @@ public class ByteApiController extends BaseController {
|
|||
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);
|
||||
if (aiManager == null) {
|
||||
return AjaxResult.error("invalid functionType");
|
||||
|
|
@ -318,10 +352,6 @@ public class ByteApiController extends BaseController {
|
|||
return AjaxResult.error("firstUrl is null");
|
||||
}
|
||||
|
||||
String nanoType = StringUtils.isNotEmpty(request.getNanoApiType()) ? request.getNanoApiType() : "v2";
|
||||
if ("pro".equalsIgnoreCase(nanoType) && StringUtils.isBlank(request.getResolution())) {
|
||||
return AjaxResult.error("pro 模式必须指定 resolution(1K、2K、4K)");
|
||||
}
|
||||
String resolution = StringUtils.isNotBlank(request.getResolution())
|
||||
? request.getResolution()
|
||||
: "1K";
|
||||
|
|
@ -348,6 +378,9 @@ public class ByteApiController extends BaseController {
|
|||
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
|
||||
resolution
|
||||
);
|
||||
if (StringUtils.isNotBlank(request.getOutputFormat())) {
|
||||
nanoRequest.setOutputFormat(request.getOutputFormat());
|
||||
}
|
||||
NanoBananaResponse nanoResponse = byteService.generateNanoBanana(nanoType, nanoRequest);
|
||||
|
||||
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
|
||||
|
|
@ -359,7 +392,8 @@ public class ByteApiController extends BaseController {
|
|||
aiOrder.setTaskId(taskId);
|
||||
aiOrder.setResult(taskId);
|
||||
|
||||
aiOrderService.orderSuccess(aiOrder);
|
||||
// 只更新任务信息,状态保持 0(生成中),等待回调
|
||||
aiOrderService.updateOrderTaskInfo(aiOrder);
|
||||
return AjaxResult.success(taskId);
|
||||
} catch (Exception e) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
|
|
@ -476,7 +510,7 @@ public class ByteApiController extends BaseController {
|
|||
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
||||
content content = byteBodyRes.getContent();
|
||||
String videoUrl = content.getVideo_url();
|
||||
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
|
||||
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
|
||||
content.setVideo_url(videoUrl);
|
||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
||||
AiOrder aiOrder = new AiOrder();
|
||||
|
|
@ -519,7 +553,7 @@ public class ByteApiController extends BaseController {
|
|||
}
|
||||
|
||||
@PostMapping(value = "/nano-callback")
|
||||
@ApiOperation("NanoBanana生成回调")
|
||||
@ApiOperation("NanoBanana生成回调(兼容 V2 和 Pro 格式)")
|
||||
@Anonymous
|
||||
public AjaxResult nanoCallback(@RequestBody NanoBananaCallback callback) throws Exception {
|
||||
if (callback == null || callback.getData() == null) {
|
||||
|
|
@ -530,35 +564,43 @@ public class ByteApiController extends BaseController {
|
|||
String taskId = data.getTaskId();
|
||||
Integer successFlag = data.getSuccessFlag();
|
||||
|
||||
// 从 V2 或 Pro 格式中获取 resultImageUrl
|
||||
String resultImageUrl = null;
|
||||
if (data.getResponse() != null) {
|
||||
// V2 格式
|
||||
resultImageUrl = data.getResponse().getResultImageUrl();
|
||||
} else if (data.getInfo() != null) {
|
||||
// Pro 格式
|
||||
resultImageUrl = data.getInfo().getResultImageUrl();
|
||||
}
|
||||
|
||||
// 根据 successFlag 处理不同状态
|
||||
if (successFlag == 1 && data.getResponse() != null && data.getResponse().getResultImageUrl() != null) {
|
||||
// 成功 - 上传到S3并更新订单
|
||||
String imageUrl = data.getResponse().getResultImageUrl();
|
||||
String s3Url = awsS3Util.uploadFileByUrl(imageUrl);
|
||||
if (successFlag == 1 && resultImageUrl != null) {
|
||||
// 成功 - 上传到腾讯云COS并更新订单状态为 1(成功)
|
||||
String s3Url = tencentCosUtil.uploadFileByUrl(resultImageUrl);
|
||||
|
||||
if (s3Url != null) {
|
||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(taskId);
|
||||
if (aiOrderByResult != null) {
|
||||
AiOrder aiOrder = new AiOrder();
|
||||
aiOrder.setId(aiOrderByResult.getId());
|
||||
aiOrder.setResult(s3Url);
|
||||
aiOrder.setTaskId(taskId);
|
||||
aiOrder.setStatus(1);
|
||||
aiOrderService.updateAiOrder(aiOrder);
|
||||
aiOrderByResult.setResult(s3Url);
|
||||
aiOrderByResult.setTaskId(taskId);
|
||||
// orderSuccess 会设置 status=1 并更新统计
|
||||
aiOrderService.orderSuccess(aiOrderByResult);
|
||||
return AjaxResult.success("回调处理成功,图像已上传");
|
||||
}
|
||||
}
|
||||
return AjaxResult.error("图像上传失败");
|
||||
} else if (successFlag == 3 || successFlag == 2) {
|
||||
// 失败 - 退款
|
||||
} else if (successFlag == 0) {
|
||||
// 生成中 - 订单初始状态已经是 0,不需要额外更新
|
||||
return AjaxResult.success("任务生成中");
|
||||
} else if (successFlag == 2 || successFlag == 3) {
|
||||
// 失败 - 退款并更新订单状态为 2(失败)
|
||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(taskId);
|
||||
if (aiOrderByResult != null) {
|
||||
// orderFailure 会设置 status=2 并处理退款
|
||||
aiOrderService.orderFailure(aiOrderByResult);
|
||||
return AjaxResult.success("回调处理失败,已退款");
|
||||
}
|
||||
} else if (successFlag == 0) {
|
||||
// 生成中 - 可忽略或记录
|
||||
return AjaxResult.success("任务生成中");
|
||||
}
|
||||
|
||||
return AjaxResult.success("回调已接收");
|
||||
|
|
@ -575,7 +617,7 @@ public class ByteApiController extends BaseController {
|
|||
String id = byteBodyRes.getId();
|
||||
content content = byteBodyRes.getContent();
|
||||
String videoUrl = content.getVideo_url();
|
||||
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
|
||||
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
|
||||
content.setVideo_url(videoUrl);
|
||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
||||
if (aiOrderByResult != null) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.ruoyi.api;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.AwsS3Util;
|
||||
import com.ruoyi.common.utils.TencentCosUtil;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
|
|
@ -18,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||
@Api(tags = "文件上传")
|
||||
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||
public class FileController {
|
||||
private final AwsS3Util awsS3Util;
|
||||
private final TencentCosUtil tencentCosUtil;
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
|
|
@ -30,7 +30,7 @@ public class FileController {
|
|||
@ApiParam(name = "file", value = "文件", required = true)
|
||||
@RequestParam("file") MultipartFile file) throws Exception {
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
String uploadUrl = awsS3Util.uploadMultipartFile(file, true);
|
||||
String uploadUrl = tencentCosUtil.uploadMultipartFile(file);
|
||||
ajax.put("url", uploadUrl);
|
||||
return ajax;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,4 +43,7 @@ public class ByteApiRequest {
|
|||
@ApiModelProperty(name = "NanoBanana API 类型: v1 | v2 | pro")
|
||||
private String nanoApiType;
|
||||
|
||||
@ApiModelProperty(name = "输出格式:png / jpg")
|
||||
private String outputFormat;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,4 +40,7 @@ public class NanoBananaPortalImgRequest {
|
|||
|
||||
@ApiModelProperty("多张参考图 URL(优先于 imageUrl)")
|
||||
private List<String> imageUrls;
|
||||
|
||||
@ApiModelProperty("输出格式:png / jpg")
|
||||
private String outputFormat;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,4 +32,7 @@ public class NanoBananaPortalRequest {
|
|||
|
||||
@ApiModelProperty("标签字符串,与旧接口一致")
|
||||
private String tags;
|
||||
|
||||
@ApiModelProperty("输出格式:png / jpg")
|
||||
private String outputFormat;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,10 +203,11 @@ google:
|
|||
client-secret:
|
||||
redirect-uri:
|
||||
|
||||
tencentCos:
|
||||
accessKey:
|
||||
tencent:
|
||||
cos:
|
||||
secretId:
|
||||
secretKey:
|
||||
endpoint:
|
||||
region:
|
||||
bucketName:
|
||||
domain:
|
||||
|
||||
|
|
@ -226,7 +227,7 @@ nanobanana:
|
|||
# NanoBanana API Token (Bearer Token)
|
||||
token: 1e95b160056f4579a9949d2516f4a463
|
||||
# 回调地址,需替换为实际部署域名 (POST接口)
|
||||
callbackUrl: https://your-domain.com/api/ai/nano-callback
|
||||
callbackUrl: https://47.86.170.114:8013/api/ai/nano-callback
|
||||
# NanoBanana API 基础地址(路径在代码中拼接)
|
||||
apiUrl: https://api.nanobananaapi.ai
|
||||
|
||||
|
|
|
|||
|
|
@ -3,82 +3,211 @@ package com.ruoyi.common.utils;
|
|||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.ClientConfig;
|
||||
import com.qcloud.cos.auth.BasicCOSCredentials;
|
||||
import com.qcloud.cos.http.HttpProtocol;
|
||||
import com.qcloud.cos.auth.COSCredentials;
|
||||
import com.qcloud.cos.model.ObjectMetadata;
|
||||
import com.qcloud.cos.model.PutObjectRequest;
|
||||
import com.qcloud.cos.model.PutObjectResult;
|
||||
import com.qcloud.cos.region.Region;
|
||||
import org.joda.time.DateTime;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS 文件上传工具类
|
||||
*/
|
||||
@Component
|
||||
public class TencentCosUtil {
|
||||
// 设置好账号的ACCESS_KEY和SECRET_KEY
|
||||
@Value("${tencentCos.accessKey}")
|
||||
private String ACCESS_KEY;
|
||||
@Value("${tencentCos.secretKey}")
|
||||
|
||||
// -------------------------- 配置参数 --------------------------
|
||||
@Value("${tencent.cos.secretId}")
|
||||
private String SECRET_ID;
|
||||
@Value("${tencent.cos.secretKey}")
|
||||
private String SECRET_KEY;
|
||||
// 要上传的空间(创建空间的名称)
|
||||
@Value("${tencentCos.bucketName}")
|
||||
private String bucketName;
|
||||
@Value("${tencentCos.endpoint}")
|
||||
private String endpoint;
|
||||
// 使用的是测试域名
|
||||
@Value("${tencentCos.domain}")
|
||||
@Value("${tencent.cos.region}")
|
||||
private String REGION;
|
||||
@Value("${tencent.cos.bucketName}")
|
||||
private String BUCKET_NAME;
|
||||
@Value("${tencent.cos.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);
|
||||
}
|
||||
|
||||
//文件上传
|
||||
public String upload(MultipartFile file) {
|
||||
/**
|
||||
* 上传MultipartFile到COS,返回文件访问地址(兼容旧代码)
|
||||
*
|
||||
* @param file 前端上传的MultipartFile
|
||||
* @return 文件访问地址(URL字符串)
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public String upload(MultipartFile file) throws Exception {
|
||||
return uploadMultipartFile(file);
|
||||
}
|
||||
|
||||
// 3 生成 cos 客户端。
|
||||
COSClient cosClient = createCosClient();
|
||||
/**
|
||||
* 上传MultipartFile到COS,返回文件访问地址
|
||||
*
|
||||
* @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 {
|
||||
//获取上传文件输入流
|
||||
InputStream inputStream = file.getInputStream();
|
||||
ObjectMetadata objectMetadata = new ObjectMetadata();
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||
bucketName,
|
||||
key,
|
||||
inputStream,
|
||||
objectMetadata);
|
||||
// 高级接口会返回一个异步结果Upload
|
||||
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
||||
String cosKey = generateCosKey(file.getOriginalFilename());
|
||||
|
||||
//返回上传文件路径
|
||||
//https://ggkt-atguigu-1310644373.cos.ap-beijing.myqcloud.com/01.jpg
|
||||
String url = domain + "/" + key;
|
||||
return url;
|
||||
} catch (Exception e) {
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, cosKey, tempPath.toFile());
|
||||
PutObjectResult putObjectResult = getCosClient().putObject(putObjectRequest);
|
||||
|
||||
return domain + cosKey;
|
||||
} finally {
|
||||
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();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
private COSClient createCosClient() {
|
||||
//1.1 初始化用户身份信息
|
||||
BasicCOSCredentials credentials = new BasicCOSCredentials(ACCESS_KEY, SECRET_KEY);
|
||||
//1.2 设置bucket的地域
|
||||
Region region = new Region(endpoint);
|
||||
ClientConfig clientConfig = new ClientConfig(region);
|
||||
// 这里建议设置使用 https 协议
|
||||
clientConfig.setHttpProtocol(HttpProtocol.https);
|
||||
//1.3 生成cos客户端
|
||||
return new COSClient(credentials, clientConfig);
|
||||
// 辅助方法:从URL提取文件名
|
||||
private String extractFileNameFromUrl(String fileUrl) {
|
||||
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
|
||||
if (fileName.contains("?")) {
|
||||
fileName = fileName.split("\\?")[0];
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,15 @@ public class NanoBananaCallback {
|
|||
private String completeTime;
|
||||
|
||||
/**
|
||||
* 响应数据(包含图像URL)
|
||||
* 响应数据(V2 格式:包含图像URL)
|
||||
*/
|
||||
private NanoBananaCallbackResponse response;
|
||||
|
||||
/**
|
||||
* 信息数据(Pro 格式:包含图像URL)
|
||||
*/
|
||||
private NanoBananaCallbackInfo info;
|
||||
|
||||
/**
|
||||
* 生成状态标志:
|
||||
* 0-生成中, 1-成功, 2-创建失败, 3-生成失败
|
||||
|
|
@ -98,4 +103,14 @@ public class NanoBananaCallback {
|
|||
@JsonProperty("resultImageUrl")
|
||||
private String resultImageUrl;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class NanoBananaCallbackInfo {
|
||||
|
||||
/**
|
||||
* 结果图像URL(Pro 格式成功时存在)
|
||||
*/
|
||||
@JsonProperty("resultImageUrl")
|
||||
private String resultImageUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ public class NanoBananaRequest {
|
|||
*/
|
||||
private Integer numImages = 1;
|
||||
|
||||
/**
|
||||
* 输出格式:png / jpg
|
||||
*/
|
||||
private String outputFormat;
|
||||
|
||||
@JsonProperty("callBackUrl")
|
||||
private String callBackUrl;
|
||||
|
||||
|
|
|
|||
|
|
@ -80,4 +80,9 @@ public interface IAiOrderService {
|
|||
AiOrder getAiOrderByResult(String result);
|
||||
|
||||
BigDecimal getSumAmountByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 更新订单任务信息(提交任务时调用)
|
||||
*/
|
||||
void updateOrderTaskInfo(AiOrder aiOrder);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,15 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
aiStatisticsService.saveOrUpdateData(aiStatistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单任务信息(提交任务时调用,不更新统计)
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateOrderTaskInfo(AiOrder aiOrder) {
|
||||
aiOrderMapper.updateById(aiOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiOrder getAiOrderByResult(String result) {
|
||||
LambdaQueryWrapper<AiOrder> query = Wrappers.lambdaQuery();
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ public class ByteService implements IByteService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 官方 /generate 与 /generate-2 共用字段:prompt、type、callBackUrl、image_size、numImages、imageUrls
|
||||
* 官方 /generate 与 /generate-2 共用字段:prompt、type、callBackUrl、image_size、numImages、imageUrls、outputFormat
|
||||
*/
|
||||
private String buildGenerateStyleBody(NanoBananaRequest req) throws Exception {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
|
|
@ -134,11 +134,14 @@ public class ByteService implements IByteService {
|
|||
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
|
||||
* 官方 /generate-pro:prompt、callBackUrl、aspectRatio、resolution、imageUrls、outputFormat
|
||||
*/
|
||||
private String buildProStyleBody(NanoBananaRequest req) throws Exception {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
|
|
@ -151,6 +154,9 @@ public class ByteService implements IByteService {
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue