修改样式
This commit is contained in:
parent
d9abd6939c
commit
8210bf0ad3
@ -62,6 +62,10 @@ export class AI3DService {
|
||||
const externalTaskId = await this.ai3dProvider.submitTask(
|
||||
dto.inputType,
|
||||
dto.inputContent,
|
||||
{
|
||||
generateType: dto.generateType,
|
||||
faceCount: dto.faceCount,
|
||||
},
|
||||
);
|
||||
|
||||
// 4. 更新状态为处理中
|
||||
|
||||
@ -1,4 +1,22 @@
|
||||
import { IsString, IsIn, IsNotEmpty, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
MaxLength,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* 模型生成类型
|
||||
* Normal: 带纹理
|
||||
* LowPoly: 低多边形
|
||||
* Geometry: 白模
|
||||
* Sketch: 草图
|
||||
*/
|
||||
export type GenerateType = 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
||||
|
||||
export class CreateTaskDto {
|
||||
@IsString()
|
||||
@ -9,4 +27,17 @@ export class CreateTaskDto {
|
||||
@IsNotEmpty({ message: '输入内容不能为空' })
|
||||
@MaxLength(2000, { message: '输入内容最多2000个字符' })
|
||||
inputContent: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['Normal', 'LowPoly', 'Geometry', 'Sketch'], {
|
||||
message: '模型类型必须是 Normal、LowPoly、Geometry 或 Sketch',
|
||||
})
|
||||
generateType?: GenerateType;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt({ message: '模型面数必须是整数' })
|
||||
@Min(10000, { message: '模型面数最小为10000' })
|
||||
@Max(1500000, { message: '模型面数最大为1500000' })
|
||||
faceCount?: number;
|
||||
}
|
||||
|
||||
@ -11,6 +11,16 @@ export interface AI3DGenerateResult {
|
||||
errorMessage?: string; // 错误信息
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型生成配置选项
|
||||
*/
|
||||
export interface AI3DGenerateOptions {
|
||||
/** 模型生成类型:Normal-带纹理, LowPoly-低多边形, Geometry-白模, Sketch-草图 */
|
||||
generateType?: 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
||||
/** 模型面数:10000-1500000,默认500000 */
|
||||
faceCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 3D 服务提供者接口
|
||||
* 支持 Mock、腾讯混元、Meshy 等实现
|
||||
@ -20,11 +30,13 @@ export interface AI3DProvider {
|
||||
* 提交生成任务
|
||||
* @param inputType 输入类型:text | image
|
||||
* @param inputContent 输入内容:文字描述或图片URL
|
||||
* @param options 可选配置项(仅文生3D支持)
|
||||
* @returns 外部任务ID
|
||||
*/
|
||||
submitTask(
|
||||
inputType: 'text' | 'image',
|
||||
inputContent: string,
|
||||
options?: AI3DGenerateOptions,
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import { AI3DProvider, AI3DGenerateResult } from './ai-3d-provider.interface';
|
||||
import {
|
||||
AI3DProvider,
|
||||
AI3DGenerateResult,
|
||||
AI3DGenerateOptions,
|
||||
} from './ai-3d-provider.interface';
|
||||
import { TencentCloudSigner } from '../utils/tencent-cloud-sign';
|
||||
import { ZipHandler } from '../utils/zip-handler';
|
||||
|
||||
@ -43,6 +47,7 @@ export class HunyuanAI3DProvider implements AI3DProvider {
|
||||
async submitTask(
|
||||
inputType: 'text' | 'image',
|
||||
inputContent: string,
|
||||
options?: AI3DGenerateOptions,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 构造请求参数
|
||||
@ -51,7 +56,19 @@ export class HunyuanAI3DProvider implements AI3DProvider {
|
||||
if (inputType === 'text') {
|
||||
// 文生3D:使用 Prompt
|
||||
payload.Prompt = inputContent;
|
||||
this.logger.log(`提交文生3D任务: ${inputContent.substring(0, 50)}...`);
|
||||
|
||||
// 文生3D支持额外参数
|
||||
if (options?.generateType) {
|
||||
payload.GenerateType = options.generateType;
|
||||
}
|
||||
if (options?.faceCount) {
|
||||
payload.FaceCount = options.faceCount;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`提交文生3D任务: ${inputContent.substring(0, 50)}... ` +
|
||||
`[类型: ${options?.generateType || 'Normal'}, 面数: ${options?.faceCount || 500000}]`,
|
||||
);
|
||||
} else {
|
||||
// 图生3D:使用 ImageUrl 或 ImageBase64
|
||||
if (
|
||||
@ -59,14 +76,20 @@ export class HunyuanAI3DProvider implements AI3DProvider {
|
||||
inputContent.startsWith('https://')
|
||||
) {
|
||||
payload.ImageUrl = inputContent;
|
||||
this.logger.log(`提交图生3D任务 (URL): ${inputContent}`);
|
||||
} else {
|
||||
// 假设是 Base64 编码的图片
|
||||
payload.ImageBase64 = inputContent;
|
||||
this.logger.log(
|
||||
`提交图生3D任务 (Base64): ${inputContent.substring(0, 30)}...`,
|
||||
);
|
||||
}
|
||||
|
||||
// 图生3D也支持模型类型
|
||||
if (options?.generateType) {
|
||||
payload.GenerateType = options.generateType;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`提交图生3D任务: ${inputContent.substring(0, 50)}... ` +
|
||||
`[类型: ${options?.generateType || 'Normal'}]`,
|
||||
);
|
||||
}
|
||||
|
||||
// 生成签名和请求头
|
||||
|
||||
@ -40,12 +40,21 @@ export interface AI3DTask {
|
||||
completeTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型生成类型
|
||||
*/
|
||||
export type AI3DGenerateType = "Normal" | "LowPoly" | "Geometry" | "Sketch";
|
||||
|
||||
/**
|
||||
* 创建任务参数
|
||||
*/
|
||||
export interface CreateAI3DTaskParams {
|
||||
inputType: AI3DInputType;
|
||||
inputContent: string;
|
||||
/** 模型生成类型:Normal-带纹理, LowPoly-低多边形, Geometry-白模, Sketch-草图 */
|
||||
generateType?: AI3DGenerateType;
|
||||
/** 模型面数:10000-1500000,默认500000 */
|
||||
faceCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -567,10 +567,15 @@ $gradient-card: linear-gradient(
|
||||
// ==========================================
|
||||
.model-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 300px);
|
||||
grid-template-columns: repeat(2, 300px);
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
justify-content: center;
|
||||
|
||||
// 只有1个卡片时居中
|
||||
&:has(.model-card:only-child) {
|
||||
grid-template-columns: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -43,14 +43,17 @@
|
||||
<div class="input-hint">
|
||||
用文字描述你的想法,AI 将为你生成精美的 3D 模型
|
||||
</div>
|
||||
<div class="textarea-wrapper">
|
||||
<a-textarea
|
||||
v-model:value="textContent"
|
||||
placeholder="例如:一只卡通风格的橙色小猫,蓝色的大眼睛,尾巴卷曲..."
|
||||
:rows="6"
|
||||
:maxlength="150"
|
||||
show-count
|
||||
:rows="5"
|
||||
:maxlength="1024"
|
||||
class="text-input"
|
||||
/>
|
||||
<span class="char-count">{{ textContent.length }}/1024</span>
|
||||
</div>
|
||||
|
||||
<div class="sample-section">
|
||||
<div class="sample-header">
|
||||
<span class="sample-title">灵感推荐</span>
|
||||
@ -67,6 +70,31 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型设置 -->
|
||||
<div class="model-settings">
|
||||
<div class="setting-row">
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">模型类型</span>
|
||||
<a-select v-model:value="generateType" class="setting-select">
|
||||
<a-select-option value="Normal">带纹理</a-select-option>
|
||||
<a-select-option value="LowPoly">低多边形</a-select-option>
|
||||
<a-select-option value="Geometry">白模</a-select-option>
|
||||
<a-select-option value="Sketch">草图</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">模型面数</span>
|
||||
<a-select v-model:value="faceCount" class="setting-select">
|
||||
<a-select-option :value="50000">5万面</a-select-option>
|
||||
<a-select-option :value="100000">10万面</a-select-option>
|
||||
<a-select-option :value="300000">30万面</a-select-option>
|
||||
<a-select-option :value="500000">50万面</a-select-option>
|
||||
<a-select-option :value="1000000">100万面</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生3D上传 -->
|
||||
@ -79,18 +107,40 @@
|
||||
上传参考图片,AI 将智能识别并生成 3D 模型
|
||||
</div>
|
||||
<a-upload-dragger
|
||||
v-model:file-list="imageFileList"
|
||||
:file-list="[]"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:show-upload-list="false"
|
||||
:max-count="1"
|
||||
accept="image/*"
|
||||
class="image-upload"
|
||||
>
|
||||
<template v-if="imageUrl">
|
||||
<img :src="imageUrl" alt="预览" class="upload-preview-image" />
|
||||
<div class="upload-change-hint">点击更换图片</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="upload-icon">
|
||||
<PictureOutlined />
|
||||
</p>
|
||||
<p class="upload-text">点击或拖拽图片到此处</p>
|
||||
<p class="upload-hint">支持 JPG、PNG 格式,最大 10MB</p>
|
||||
</template>
|
||||
</a-upload-dragger>
|
||||
|
||||
<!-- 模型类型选择 -->
|
||||
<div class="model-settings">
|
||||
<div class="setting-row">
|
||||
<div class="setting-item full-width">
|
||||
<span class="setting-label">模型类型</span>
|
||||
<a-select v-model:value="generateType" class="setting-select">
|
||||
<a-select-option value="Normal">带纹理</a-select-option>
|
||||
<a-select-option value="LowPoly">低多边形</a-select-option>
|
||||
<a-select-option value="Geometry">白模</a-select-option>
|
||||
<a-select-option value="Sketch">草图</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -309,7 +359,6 @@ import {
|
||||
ArrowLeftOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import type { UploadFile } from "ant-design-vue"
|
||||
import {
|
||||
createAI3DTask,
|
||||
getAI3DTasks,
|
||||
@ -358,11 +407,14 @@ const samplePrompts = [
|
||||
// 状态
|
||||
const inputType = ref<"text" | "image">("text")
|
||||
const textContent = ref("")
|
||||
const imageFileList = ref<UploadFile[]>()
|
||||
const imageUrl = ref("")
|
||||
const generating = ref(false)
|
||||
const currentSampleIndex = ref(0)
|
||||
|
||||
// 模型设置
|
||||
const generateType = ref<"Normal" | "LowPoly" | "Geometry" | "Sketch">("Normal")
|
||||
const faceCount = ref(500000)
|
||||
|
||||
// 历史记录
|
||||
const historyList = ref<AI3DTask[]>([])
|
||||
const historyLoading = ref(false)
|
||||
@ -448,12 +500,18 @@ const handleBeforeUpload = async (file: File) => {
|
||||
|
||||
// 上传图片
|
||||
try {
|
||||
const result = await uploadFile(file)
|
||||
imageUrl.value = result.url
|
||||
const result: any = await uploadFile(file)
|
||||
// 兼容不同的响应格式
|
||||
const url = result.data?.url || result.url
|
||||
if (url) {
|
||||
imageUrl.value = url
|
||||
message.success("图片上传成功")
|
||||
} else {
|
||||
message.error("上传失败:无法获取图片地址")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("上传失败:", error)
|
||||
message.error("图片上传失败")
|
||||
return false
|
||||
}
|
||||
|
||||
return false // 阻止默认上传行为
|
||||
@ -468,16 +526,24 @@ const handleGenerate = async () => {
|
||||
const content =
|
||||
inputType.value === "text" ? textContent.value.trim() : imageUrl.value
|
||||
|
||||
const task = await createAI3DTask({
|
||||
// 构建请求参数
|
||||
const params: any = {
|
||||
inputType: inputType.value,
|
||||
inputContent: content,
|
||||
})
|
||||
generateType: generateType.value,
|
||||
}
|
||||
|
||||
// 文生3D时添加模型面数参数
|
||||
if (inputType.value === "text") {
|
||||
params.faceCount = faceCount.value
|
||||
}
|
||||
|
||||
const task = await createAI3DTask(params)
|
||||
|
||||
// 清空输入
|
||||
if (inputType.value === "text") {
|
||||
textContent.value = ""
|
||||
} else {
|
||||
imageFileList.value = []
|
||||
imageUrl.value = ""
|
||||
}
|
||||
|
||||
@ -943,6 +1009,19 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
position: relative;
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-input {
|
||||
background: rgba($surface-light, 0.6) !important;
|
||||
border: 2px solid rgba($primary, 0.15) !important;
|
||||
@ -950,6 +1029,7 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
color: $text;
|
||||
resize: none;
|
||||
transition: all 0.3s;
|
||||
padding-bottom: 28px !important;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba($text-muted, 0.5);
|
||||
@ -964,14 +1044,67 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
box-shadow: 0 0 0 4px rgba($primary, 0.1) !important;
|
||||
background: rgba($surface-light, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input-data-count) {
|
||||
color: $text-muted;
|
||||
// 模型设置
|
||||
.model-settings {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: rgba($primary, 0.05);
|
||||
border: 1px solid rgba($primary, 0.15);
|
||||
border-radius: 12px;
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&.full-width {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
width: 100%;
|
||||
|
||||
:deep(.ant-select-selector) {
|
||||
background: rgba($surface, 0.8) !important;
|
||||
border: 1px solid rgba($primary, 0.2) !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
|
||||
.ant-select-selection-item {
|
||||
line-height: 34px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover :deep(.ant-select-selector) {
|
||||
border-color: rgba($primary, 0.4) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused :deep(.ant-select-selector) {
|
||||
border-color: $primary !important;
|
||||
box-shadow: 0 0 0 2px rgba($primary, 0.1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sample-section {
|
||||
margin-top: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sample-header {
|
||||
@ -1028,28 +1161,51 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
border: 2px dashed rgba($primary, 0.3) !important;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-light !important;
|
||||
background: rgba($surface-light, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
:deep(.ant-upload) {
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
:deep(.upload-icon) {
|
||||
color: $primary-light;
|
||||
font-size: 56px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
:deep(.upload-text) {
|
||||
color: $text;
|
||||
font-size: 15px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
:deep(.upload-hint) {
|
||||
color: $text-muted;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.upload-preview-image) {
|
||||
max-width: 100%;
|
||||
max-height: 180px;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.upload-change-hint) {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: $text-muted;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-drag:hover .upload-change-hint) {
|
||||
color: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user