修改图生3D,支持多图上传
This commit is contained in:
parent
a8b9f658a0
commit
7f8e28a4af
@ -62,13 +62,44 @@ export class AI3DService {
|
|||||||
|
|
||||||
// 3. 提交到 AI 服务
|
// 3. 提交到 AI 服务
|
||||||
try {
|
try {
|
||||||
|
// 构建生成选项
|
||||||
|
const options: any = {
|
||||||
|
generateType: dto.generateType,
|
||||||
|
faceCount: dto.faceCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理多视图图片(图生3D支持)
|
||||||
|
if (dto.inputType === 'image' && dto.multiViewImages) {
|
||||||
|
const viewKeyMap: Record<string, string> = {
|
||||||
|
left: 'left',
|
||||||
|
right: 'right',
|
||||||
|
back: 'back',
|
||||||
|
top: 'top',
|
||||||
|
bottom: 'bottom',
|
||||||
|
left45: 'left_front',
|
||||||
|
right45: 'right_front',
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiViewImages: { viewType: string; imageUrl: string }[] = [];
|
||||||
|
for (const [key, url] of Object.entries(dto.multiViewImages)) {
|
||||||
|
if (url && key !== 'front' && viewKeyMap[key]) {
|
||||||
|
multiViewImages.push({
|
||||||
|
viewType: viewKeyMap[key],
|
||||||
|
imageUrl: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiViewImages.length > 0) {
|
||||||
|
options.multiViewImages = multiViewImages;
|
||||||
|
this.logger.log(`多视图模式: ${multiViewImages.length} 张额外视图`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const externalTaskId = await this.ai3dProvider.submitTask(
|
const externalTaskId = await this.ai3dProvider.submitTask(
|
||||||
dto.inputType,
|
dto.inputType,
|
||||||
dto.inputContent,
|
dto.inputContent,
|
||||||
{
|
options,
|
||||||
generateType: dto.generateType,
|
|
||||||
faceCount: dto.faceCount,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. 更新状态为处理中
|
// 4. 更新状态为处理中
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
IsInt,
|
IsInt,
|
||||||
Min,
|
Min,
|
||||||
Max,
|
Max,
|
||||||
|
IsObject,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,6 +19,21 @@ import {
|
|||||||
*/
|
*/
|
||||||
export type GenerateType = 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
export type GenerateType = 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多视图图片类型
|
||||||
|
* 支持的视角:left, right, back, top, bottom, left_front (左前45°), right_front (右前45°)
|
||||||
|
*/
|
||||||
|
export type MultiViewImages = {
|
||||||
|
front?: string; // 正图(作为主图)
|
||||||
|
left?: string; // 左视图
|
||||||
|
right?: string; // 右视图
|
||||||
|
back?: string; // 后视图
|
||||||
|
top?: string; // 顶视图
|
||||||
|
bottom?: string; // 底视图
|
||||||
|
left45?: string; // 左前45°视图 -> left_front
|
||||||
|
right45?: string; // 右前45°视图 -> right_front
|
||||||
|
};
|
||||||
|
|
||||||
export class CreateTaskDto {
|
export class CreateTaskDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsIn(['text', 'image'], { message: '输入类型必须是 text 或 image' })
|
@IsIn(['text', 'image'], { message: '输入类型必须是 text 或 image' })
|
||||||
@ -40,4 +56,8 @@ export class CreateTaskDto {
|
|||||||
@Min(10000, { message: '模型面数最小为10000' })
|
@Min(10000, { message: '模型面数最小为10000' })
|
||||||
@Max(1500000, { message: '模型面数最大为1500000' })
|
@Max(1500000, { message: '模型面数最大为1500000' })
|
||||||
faceCount?: number;
|
faceCount?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject({ message: '多视图图片必须是对象' })
|
||||||
|
multiViewImages?: MultiViewImages;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,14 @@ export interface AI3DGenerateResult {
|
|||||||
errorMessage?: string; // 错误信息
|
errorMessage?: string; // 错误信息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多视图图片
|
||||||
|
*/
|
||||||
|
export interface MultiViewImage {
|
||||||
|
viewType: string; // left, right, back, top, bottom, left_front, right_front
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模型生成配置选项
|
* 模型生成配置选项
|
||||||
*/
|
*/
|
||||||
@ -19,6 +27,8 @@ export interface AI3DGenerateOptions {
|
|||||||
generateType?: 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
generateType?: 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
||||||
/** 模型面数:10000-1500000,默认500000 */
|
/** 模型面数:10000-1500000,默认500000 */
|
||||||
faceCount?: number;
|
faceCount?: number;
|
||||||
|
/** 多视图图片(图生3D支持) */
|
||||||
|
multiViewImages?: MultiViewImage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -85,11 +85,23 @@ export class HunyuanAI3DProvider implements AI3DProvider {
|
|||||||
payload.GenerateType = options.generateType;
|
payload.GenerateType = options.generateType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 多视图图片支持
|
||||||
|
if (options?.multiViewImages && options.multiViewImages.length > 0) {
|
||||||
|
payload.MultiViewImages = options.multiViewImages.map((img) => ({
|
||||||
|
ViewType: img.viewType,
|
||||||
|
ViewImageUrl: img.imageUrl,
|
||||||
|
}));
|
||||||
|
this.logger.log(
|
||||||
|
`提交图生3D任务(多视图): ${options.multiViewImages.length} 张图片 ` +
|
||||||
|
`[类型: ${options?.generateType || 'Normal'}]`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`提交图生3D任务: ${inputContent.substring(0, 50)}... ` +
|
`提交图生3D任务: ${inputContent.substring(0, 50)}... ` +
|
||||||
`[类型: ${options?.generateType || 'Normal'}]`,
|
`[类型: ${options?.generateType || 'Normal'}]`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成签名和请求头
|
// 生成签名和请求头
|
||||||
const headers = TencentCloudSigner.sign({
|
const headers = TencentCloudSigner.sign({
|
||||||
|
|||||||
@ -111,13 +111,71 @@
|
|||||||
|
|
||||||
<!-- 图生3D上传 -->
|
<!-- 图生3D上传 -->
|
||||||
<div v-else class="image-input-section">
|
<div v-else class="image-input-section">
|
||||||
<div class="input-label">
|
<!-- 单张/多张图片切换 -->
|
||||||
<PictureOutlined class="label-icon" />
|
<div class="image-mode-switch">
|
||||||
<span>参考图片</span>
|
<span
|
||||||
|
class="mode-link"
|
||||||
|
:class="{ active: imageMode === 'single' }"
|
||||||
|
@click="imageMode = 'single'"
|
||||||
|
>单张图片</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mode-link"
|
||||||
|
:class="{ active: imageMode === 'multiple' }"
|
||||||
|
@click="imageMode = 'multiple'"
|
||||||
|
>多张图片</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 图片上传建议 Popover -->
|
||||||
|
<a-popover
|
||||||
|
placement="rightTop"
|
||||||
|
trigger="click"
|
||||||
|
:overlayStyle="{ width: '320px' }"
|
||||||
|
overlayClassName="image-tips-popover"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="image-tips-content">
|
||||||
|
<div class="tips-desc">
|
||||||
|
支持png/jpg/jpeg/webp格式,图片大小最大不超过10M,分辨率最低要求128x128
|
||||||
</div>
|
</div>
|
||||||
<div class="input-hint">
|
<div class="tips-example">
|
||||||
上传参考图片,AI 将智能识别并生成 3D 模型
|
<img
|
||||||
|
src="https://competition-ms-1325825530.cos.ap-guangzhou.myqcloud.com/img/3d-example.png"
|
||||||
|
alt="示例图片"
|
||||||
|
/>
|
||||||
|
<div class="tips-list">
|
||||||
|
<div class="tip-item">
|
||||||
|
<CheckCircleFilled class="tip-icon" />
|
||||||
|
<span>背景简单(尽量纯色)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<CheckCircleFilled class="tip-icon" />
|
||||||
|
<span>不包含文字</span>
|
||||||
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<CheckCircleFilled class="tip-icon" />
|
||||||
|
<span>单主体</span>
|
||||||
|
</div>
|
||||||
|
<div class="tip-item">
|
||||||
|
<CheckCircleFilled class="tip-icon" />
|
||||||
|
<span>主体不要过小</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<span class="tips-title">图片上传建议</span>
|
||||||
|
</template>
|
||||||
|
<span class="image-tips-link">
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
<span>图片上传建议</span>
|
||||||
|
</span>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 单张图片模式 -->
|
||||||
|
<template v-if="imageMode === 'single'">
|
||||||
<a-upload-dragger
|
<a-upload-dragger
|
||||||
:file-list="[]"
|
:file-list="[]"
|
||||||
:before-upload="handleBeforeUpload"
|
:before-upload="handleBeforeUpload"
|
||||||
@ -138,6 +196,39 @@
|
|||||||
<p class="upload-hint">支持 JPG、PNG 格式,最大 10MB</p>
|
<p class="upload-hint">支持 JPG、PNG 格式,最大 10MB</p>
|
||||||
</template>
|
</template>
|
||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 多张图片模式 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="multi-image-container">
|
||||||
|
<!-- 缩略图展示条 -->
|
||||||
|
<div class="thumbnail-bar" @click="openMultiViewModal">
|
||||||
|
<div v-if="hasMultiImages" class="thumbnail-list">
|
||||||
|
<div
|
||||||
|
v-for="(img, key) in multiImages"
|
||||||
|
:key="key"
|
||||||
|
class="thumbnail-item"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<img :src="img" :alt="getViewName(key)" />
|
||||||
|
<div
|
||||||
|
class="thumbnail-delete"
|
||||||
|
@click="removeMultiImage(key)"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="thumbnail-add">
|
||||||
|
<PlusOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-tip">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
<span>图片上传建议:上传多角度图片可提升生成质量</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 模型类型选择 -->
|
<!-- 模型类型选择 -->
|
||||||
<div class="model-settings">
|
<div class="model-settings">
|
||||||
@ -154,6 +245,196 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 多视图上传弹框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="multiViewModalVisible"
|
||||||
|
title="添加多视图"
|
||||||
|
:width="600"
|
||||||
|
:footer="null"
|
||||||
|
class="multi-view-modal"
|
||||||
|
@cancel="multiViewModalVisible = false"
|
||||||
|
>
|
||||||
|
<div class="multi-view-grid">
|
||||||
|
<!-- 顶图 -->
|
||||||
|
<div class="view-row top-row">
|
||||||
|
<div class="view-item" @click="triggerUpload('top')">
|
||||||
|
<template v-if="multiImages.top">
|
||||||
|
<img :src="multiImages.top" alt="顶图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('top')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传顶图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左45° 正图 右45° -->
|
||||||
|
<div class="view-row middle-row">
|
||||||
|
<div class="view-item" @click="triggerUpload('left45')">
|
||||||
|
<template v-if="multiImages.left45">
|
||||||
|
<img :src="multiImages.left45" alt="左45°图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('left45')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传左45°图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="view-item required" @click="triggerUpload('front')">
|
||||||
|
<template v-if="multiImages.front">
|
||||||
|
<img :src="multiImages.front" alt="正图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('front')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传正图 <span class="required-mark">*</span></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="view-item" @click="triggerUpload('right45')">
|
||||||
|
<template v-if="multiImages.right45">
|
||||||
|
<img :src="multiImages.right45" alt="右45°图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('right45')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传右45°图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左图 中心预览 右图 -->
|
||||||
|
<div class="view-row center-row">
|
||||||
|
<div class="view-item" @click="triggerUpload('left')">
|
||||||
|
<template v-if="multiImages.left">
|
||||||
|
<img :src="multiImages.left" alt="左图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('left')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传左图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="view-center">
|
||||||
|
<div class="center-preview">
|
||||||
|
<img
|
||||||
|
v-if="multiImages.front"
|
||||||
|
:src="multiImages.front"
|
||||||
|
alt="预览"
|
||||||
|
/>
|
||||||
|
<div v-else class="center-placeholder">
|
||||||
|
<AppstoreOutlined />
|
||||||
|
<span>3D 预览</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="view-item" @click="triggerUpload('right')">
|
||||||
|
<template v-if="multiImages.right">
|
||||||
|
<img :src="multiImages.right" alt="右图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('right')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传右图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 背图 -->
|
||||||
|
<div class="view-row bottom-row">
|
||||||
|
<div class="view-item" @click="triggerUpload('back')">
|
||||||
|
<template v-if="multiImages.back">
|
||||||
|
<img :src="multiImages.back" alt="背图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('back')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传背图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底图 -->
|
||||||
|
<div class="view-row bottom-row">
|
||||||
|
<div class="view-item" @click="triggerUpload('bottom')">
|
||||||
|
<template v-if="multiImages.bottom">
|
||||||
|
<img :src="multiImages.bottom" alt="底图" />
|
||||||
|
<div
|
||||||
|
class="view-delete"
|
||||||
|
@click.stop="removeMultiImage('bottom')"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="view-placeholder">
|
||||||
|
<PictureOutlined />
|
||||||
|
<span>上传底图</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入 -->
|
||||||
|
<input
|
||||||
|
ref="multiImageInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleMultiImageUpload"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
@ -375,6 +656,11 @@ import {
|
|||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
QuestionCircleOutlined,
|
||||||
|
CheckCircleFilled,
|
||||||
} from "@ant-design/icons-vue"
|
} from "@ant-design/icons-vue"
|
||||||
import {
|
import {
|
||||||
createAI3DTask,
|
createAI3DTask,
|
||||||
@ -428,6 +714,36 @@ const imageUrl = ref("")
|
|||||||
const generating = ref(false)
|
const generating = ref(false)
|
||||||
const currentSampleIndex = ref(0)
|
const currentSampleIndex = ref(0)
|
||||||
|
|
||||||
|
// 图片模式:单张/多张
|
||||||
|
const imageMode = ref<"single" | "multiple">("single")
|
||||||
|
|
||||||
|
// 多图上传相关
|
||||||
|
type MultiImageKey =
|
||||||
|
| "top"
|
||||||
|
| "left45"
|
||||||
|
| "front"
|
||||||
|
| "right45"
|
||||||
|
| "left"
|
||||||
|
| "right"
|
||||||
|
| "back"
|
||||||
|
| "bottom"
|
||||||
|
const multiImages = ref<Partial<Record<MultiImageKey, string>>>({})
|
||||||
|
const multiViewModalVisible = ref(false)
|
||||||
|
const multiImageInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const currentUploadKey = ref<MultiImageKey | null>(null)
|
||||||
|
|
||||||
|
// 视图名称映射
|
||||||
|
const viewNameMap: Record<MultiImageKey, string> = {
|
||||||
|
top: "顶图",
|
||||||
|
left45: "左45°图",
|
||||||
|
front: "正图",
|
||||||
|
right45: "右45°图",
|
||||||
|
left: "左图",
|
||||||
|
right: "右图",
|
||||||
|
back: "背图",
|
||||||
|
bottom: "底图",
|
||||||
|
}
|
||||||
|
|
||||||
// 模型设置
|
// 模型设置
|
||||||
const generateType = ref<"Normal" | "LowPoly" | "Geometry" | "Sketch">("Normal")
|
const generateType = ref<"Normal" | "LowPoly" | "Geometry" | "Sketch">("Normal")
|
||||||
const faceCount = ref(500000)
|
const faceCount = ref(500000)
|
||||||
@ -452,6 +768,11 @@ let resizeObserver: ResizeObserver | null = null
|
|||||||
// 计算属性
|
// 计算属性
|
||||||
const currentSamples = computed(() => samplePrompts[currentSampleIndex.value])
|
const currentSamples = computed(() => samplePrompts[currentSampleIndex.value])
|
||||||
|
|
||||||
|
// 是否有多图上传
|
||||||
|
const hasMultiImages = computed(() => {
|
||||||
|
return Object.keys(multiImages.value).length > 0
|
||||||
|
})
|
||||||
|
|
||||||
const canGenerate = computed(() => {
|
const canGenerate = computed(() => {
|
||||||
// 首先检查用户是否有创建权限
|
// 首先检查用户是否有创建权限
|
||||||
if (!authStore.hasPermission("ai-3d:create")) {
|
if (!authStore.hasPermission("ai-3d:create")) {
|
||||||
@ -462,7 +783,13 @@ const canGenerate = computed(() => {
|
|||||||
if (inputType.value === "text") {
|
if (inputType.value === "text") {
|
||||||
return textContent.value.trim().length > 0
|
return textContent.value.trim().length > 0
|
||||||
} else {
|
} else {
|
||||||
|
// 图生3D模式
|
||||||
|
if (imageMode.value === "single") {
|
||||||
return imageUrl.value.length > 0
|
return imageUrl.value.length > 0
|
||||||
|
} else {
|
||||||
|
// 多图模式:至少需要正图
|
||||||
|
return !!multiImages.value.front
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -537,14 +864,76 @@ const handleBeforeUpload = async (file: File) => {
|
|||||||
return false // 阻止默认上传行为
|
return false // 阻止默认上传行为
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 多图上传相关方法
|
||||||
|
const openMultiViewModal = () => {
|
||||||
|
multiViewModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getViewName = (key: string) => {
|
||||||
|
return viewNameMap[key as MultiImageKey] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerUpload = (key: MultiImageKey) => {
|
||||||
|
currentUploadKey.value = key
|
||||||
|
multiImageInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMultiImageUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file || !currentUploadKey.value) return
|
||||||
|
|
||||||
|
const isImage = file.type.startsWith("image/")
|
||||||
|
if (!isImage) {
|
||||||
|
message.error("只能上传图片文件")
|
||||||
|
input.value = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLt10M = file.size / 1024 / 1024 < 10
|
||||||
|
if (!isLt10M) {
|
||||||
|
message.error("图片大小不能超过 10MB")
|
||||||
|
input.value = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: any = await uploadFile(file)
|
||||||
|
const url = result.data?.url || result.url
|
||||||
|
if (url) {
|
||||||
|
multiImages.value[currentUploadKey.value] = url
|
||||||
|
message.success(`${getViewName(currentUploadKey.value)}上传成功`)
|
||||||
|
} else {
|
||||||
|
message.error("上传失败:无法获取图片地址")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("上传失败:", error)
|
||||||
|
message.error("图片上传失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = ""
|
||||||
|
currentUploadKey.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMultiImage = (key: string) => {
|
||||||
|
delete multiImages.value[key as MultiImageKey]
|
||||||
|
}
|
||||||
|
|
||||||
// 生成3D模型
|
// 生成3D模型
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!canGenerate.value) return
|
if (!canGenerate.value) return
|
||||||
|
|
||||||
generating.value = true
|
generating.value = true
|
||||||
try {
|
try {
|
||||||
const content =
|
let content: string
|
||||||
inputType.value === "text" ? textContent.value.trim() : imageUrl.value
|
if (inputType.value === "text") {
|
||||||
|
content = textContent.value.trim()
|
||||||
|
} else if (imageMode.value === "single") {
|
||||||
|
content = imageUrl.value
|
||||||
|
} else {
|
||||||
|
// 多张图片模式:使用正图作为主要输入
|
||||||
|
content = multiImages.value.front || ""
|
||||||
|
}
|
||||||
|
|
||||||
// 构建请求参数
|
// 构建请求参数
|
||||||
const params: any = {
|
const params: any = {
|
||||||
@ -558,13 +947,28 @@ const handleGenerate = async () => {
|
|||||||
params.faceCount = faceCount.value
|
params.faceCount = faceCount.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 多张图片模式:添加额外的视图图片
|
||||||
|
if (inputType.value === "image" && imageMode.value === "multiple") {
|
||||||
|
const multiViewImages: Record<string, string> = {}
|
||||||
|
Object.entries(multiImages.value).forEach(([key, url]) => {
|
||||||
|
if (url) {
|
||||||
|
multiViewImages[key] = url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (Object.keys(multiViewImages).length > 0) {
|
||||||
|
params.multiViewImages = multiViewImages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const task = await createAI3DTask(params)
|
const task = await createAI3DTask(params)
|
||||||
|
|
||||||
// 清空输入
|
// 清空输入
|
||||||
if (inputType.value === "text") {
|
if (inputType.value === "text") {
|
||||||
textContent.value = ""
|
textContent.value = ""
|
||||||
} else {
|
} else if (imageMode.value === "single") {
|
||||||
imageUrl.value = ""
|
imageUrl.value = ""
|
||||||
|
} else {
|
||||||
|
multiImages.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到生成页面
|
// 跳转到生成页面
|
||||||
@ -604,7 +1008,7 @@ const startPolling = () => {
|
|||||||
|
|
||||||
pollingTimer = window.setInterval(async () => {
|
pollingTimer = window.setInterval(async () => {
|
||||||
const processingTasks = historyList.value.filter(
|
const processingTasks = historyList.value.filter(
|
||||||
(t) => t.status === "pending" || t.status === "processing"
|
(t) => t.status === "pending" || t.status === "processing",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (processingTasks.length === 0) {
|
if (processingTasks.length === 0) {
|
||||||
@ -774,7 +1178,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 检查是否有处理中的任务,有则开启轮询
|
// 检查是否有处理中的任务,有则开启轮询
|
||||||
const hasProcessing = historyList.value.some(
|
const hasProcessing = historyList.value.some(
|
||||||
(t) => t.status === "pending" || t.status === "processing"
|
(t) => t.status === "pending" || t.status === "processing",
|
||||||
)
|
)
|
||||||
if (hasProcessing) {
|
if (hasProcessing) {
|
||||||
startPolling()
|
startPolling()
|
||||||
@ -1249,6 +1653,392 @@ $gradient-secondary: linear-gradient(135deg, $primary-light 0%, $primary 100%);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 单张/多张图片切换
|
||||||
|
.image-mode-switch {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mode-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text-muted;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -4px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: $primary;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-tips-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-muted;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传建议弹框样式
|
||||||
|
.image-tips-content {
|
||||||
|
.tips-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-secondary;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-example {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-list {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text;
|
||||||
|
|
||||||
|
.tip-icon {
|
||||||
|
color: $success;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多图容器
|
||||||
|
.multi-image-container {
|
||||||
|
.thumbnail-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba($surface-light, 0.6);
|
||||||
|
border: 2px dashed rgba($primary, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
min-height: 72px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $primary-light;
|
||||||
|
background: rgba($surface-light, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: rgba($primary, 0.05);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba($primary, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-item {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .thumbnail-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-add {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 2px dashed rgba($primary, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $primary;
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $primary;
|
||||||
|
background: rgba($primary, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-muted;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多视图弹框
|
||||||
|
.multi-view-modal {
|
||||||
|
:deep(.ant-modal-content) {
|
||||||
|
background: $surface;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-header) {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-close) {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-view-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
&.middle-row,
|
||||||
|
&.center-row {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-item {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 2px dashed rgba($primary, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba($surface-light, 0.5);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $primary;
|
||||||
|
background: rgba($primary, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.required {
|
||||||
|
border-color: rgba($primary, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: $text-muted;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-mark {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 77, 79, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .view-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-center {
|
||||||
|
width: 120px;
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.center-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 2px solid rgba($primary, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba($primary, 0.05) 0%,
|
||||||
|
rgba($primary, 0.1) 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: $text-muted;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.image-upload {
|
.image-upload {
|
||||||
:deep(.ant-upload-drag) {
|
:deep(.ant-upload-drag) {
|
||||||
background: rgba($surface-light, 0.6) !important;
|
background: rgba($surface-light, 0.6) !important;
|
||||||
@ -1470,7 +2260,11 @@ $gradient-secondary: linear-gradient(135deg, $primary-light 0%, $primary 100%);
|
|||||||
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
||||||
}
|
}
|
||||||
&.gradient-2 {
|
&.gradient-2 {
|
||||||
background: linear-gradient(135deg, $primary-dark 0%, darken($primary-dark, 10%) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
$primary-dark 0%,
|
||||||
|
darken($primary-dark, 10%) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
&.gradient-3 {
|
&.gradient-3 {
|
||||||
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
|
||||||
@ -1960,3 +2754,67 @@ $gradient-secondary: linear-gradient(135deg, $primary-light 0%, $primary 100%);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- 全局样式 - 用于 Popover 等渲染到 body 的组件 -->
|
||||||
|
<style lang="scss">
|
||||||
|
// 图片上传建议 Popover 全局样式
|
||||||
|
.image-tips-popover {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-title {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-tips-content {
|
||||||
|
.tips-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-example {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-list {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
|
||||||
|
.tip-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user