mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-24 12:04:31 +08:00
AI3D 功能完成
This commit is contained in:
145
web/src/assets/css/admin/ai3d.scss
Normal file
145
web/src/assets/css/admin/ai3d.scss
Normal file
@@ -0,0 +1,145 @@
|
||||
.admin-threed-jobs {
|
||||
padding: 20px;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--theme-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
background: var(--card-bg);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--el-box-shadow, 0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
.el-select__wrapper {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.stat-card {
|
||||
background: var(--card-bg);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--el-box-shadow, 0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.processing {
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
background: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--theme-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--theme-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--el-box-shadow, 0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-detail {
|
||||
.task-params,
|
||||
.task-result,
|
||||
.task-error {
|
||||
margin-top: 20px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--theme-text-color-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.params-content {
|
||||
background: var(--card-bg);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--line-box);
|
||||
}
|
||||
}
|
||||
|
||||
.result-links {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// 3D 模型预览弹窗
|
||||
.model-preview-dialog {
|
||||
.el-dialog__body {
|
||||
padding: 0 0 16px 0;
|
||||
background: var(--el-bg-color-overlay);
|
||||
}
|
||||
|
||||
.model-preview-wrapper {
|
||||
height: calc(100vh - 125px);
|
||||
padding: 12px;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,8 +288,11 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--theme-text-color-secondary);
|
||||
min-height: 120px;
|
||||
min-height: 200px;
|
||||
max-height: 200px;
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
border: 1px solid var(--line-box);
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
@@ -542,15 +545,14 @@
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
min-height: 500px;
|
||||
height: calc(100vh - 125px);
|
||||
background: var(--chat-wel-bg);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
|
||||
.three-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
height: 100%;
|
||||
background: var(--chat-wel-bg);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
@@ -561,7 +563,7 @@
|
||||
|
||||
.preview-placeholder {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
min-height: 500px;
|
||||
background: var(--chat-wel-bg);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1756786244728') format('woff2'),
|
||||
url('iconfont.woff?t=1756786244728') format('woff'),
|
||||
url('iconfont.ttf?t=1756786244728') format('truetype');
|
||||
src: url('iconfont.woff2?t=1756954977612') format('woff2'),
|
||||
url('iconfont.woff?t=1756954977612') format('woff'),
|
||||
url('iconfont.ttf?t=1756954977612') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -14,7 +14,11 @@
|
||||
}
|
||||
|
||||
.icon-cube:before {
|
||||
content: "\e876";
|
||||
content: "\e72c";
|
||||
}
|
||||
|
||||
.icon-tag:before {
|
||||
content: "\e657";
|
||||
}
|
||||
|
||||
.icon-tencent:before {
|
||||
@@ -45,7 +49,7 @@
|
||||
content: "\e652";
|
||||
}
|
||||
|
||||
.icon-suanli:before {
|
||||
.icon-power:before {
|
||||
content: "\e651";
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,11 +6,18 @@
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "34453337",
|
||||
"name": "3D会场",
|
||||
"icon_id": "544492",
|
||||
"name": "cube",
|
||||
"font_class": "cube",
|
||||
"unicode": "e876",
|
||||
"unicode_decimal": 59510
|
||||
"unicode": "e72c",
|
||||
"unicode_decimal": 59180
|
||||
},
|
||||
{
|
||||
"icon_id": "5072110",
|
||||
"name": "tag",
|
||||
"font_class": "tag",
|
||||
"unicode": "e657",
|
||||
"unicode_decimal": 58967
|
||||
},
|
||||
{
|
||||
"icon_id": "3547761",
|
||||
@@ -64,7 +71,7 @@
|
||||
{
|
||||
"icon_id": "25677845",
|
||||
"name": "算力",
|
||||
"font_class": "suanli",
|
||||
"font_class": "power",
|
||||
"unicode": "e651",
|
||||
"unicode_decimal": 58961
|
||||
},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,34 +2,6 @@
|
||||
<div class="three-d-preview">
|
||||
<div ref="container" class="preview-container"></div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<div class="control-group">
|
||||
<label>缩放</label>
|
||||
<div class="scale-controls">
|
||||
<el-button size="small" @click="zoomOut" :disabled="scale <= 0.1">
|
||||
<el-icon><Minus /></el-icon>
|
||||
</el-button>
|
||||
<span class="scale-value">{{ scale.toFixed(1) }}x</span>
|
||||
<el-button size="small" @click="zoomIn" :disabled="scale >= 3">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>模型颜色</label>
|
||||
<div class="color-picker">
|
||||
<el-color-picker
|
||||
v-model="modelColor"
|
||||
@change="updateModelColor"
|
||||
:predefine="predefineColors"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
@@ -56,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Loading, Minus, Plus, Warning } from '@element-plus/icons-vue'
|
||||
import { Loading, Warning } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElIcon } from 'element-plus'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
@@ -82,20 +54,17 @@ const container = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const loadingProgress = ref(0)
|
||||
const scale = ref(1)
|
||||
const modelColor = ref('#00ff88')
|
||||
const predefineColors = ref([
|
||||
'#00ff88', // 亮绿色
|
||||
'#ff6b6b', // 亮红色
|
||||
'#4ecdc4', // 亮青色
|
||||
'#45b7d1', // 亮蓝色
|
||||
'#f9ca24', // 亮黄色
|
||||
'#f0932b', // 亮橙色
|
||||
'#eb4d4b', // 亮粉红
|
||||
'#6c5ce7', // 亮紫色
|
||||
'#a29bfe', // 亮靛蓝
|
||||
'#fd79a8', // 亮玫瑰
|
||||
])
|
||||
const modelType = computed(() => {
|
||||
if (props.modelType) {
|
||||
return props.modelType.toLowerCase()
|
||||
}
|
||||
// 从模型URL中获取类型
|
||||
if (props.modelUrl) {
|
||||
const url = new URL(props.modelUrl)
|
||||
return url.pathname.split('.').pop()
|
||||
}
|
||||
return 'glb'
|
||||
})
|
||||
|
||||
// Three.js 相关变量
|
||||
let scene, camera, renderer, controls, model, mixer, clock
|
||||
@@ -111,12 +80,12 @@ const initThreeJS = () => {
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0x2a2a2a) // 深灰色背景,类似截图
|
||||
scene.background = new THREE.Color(0x2d2d2d) // 深灰色背景,匹配截图效果
|
||||
|
||||
// 获取容器尺寸,确保有最小尺寸
|
||||
// 获取容器尺寸,完全自适应父容器
|
||||
const containerRect = container.value.getBoundingClientRect()
|
||||
const width = Math.max(containerRect.width || 400, 400)
|
||||
const height = Math.max(containerRect.height || 300, 300)
|
||||
const width = containerRect.width || 400
|
||||
const height = containerRect.height || 300
|
||||
|
||||
// 创建相机 - 参考截图的视角(稍微俯视,从左上角观察)
|
||||
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
||||
@@ -130,9 +99,11 @@ const initThreeJS = () => {
|
||||
})
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
renderer.shadowMap.enabled = false // 禁用阴影
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
// 提升曝光度让模型更加高亮
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
renderer.toneMappingExposure = 2.2
|
||||
|
||||
// 添加到容器
|
||||
container.value.appendChild(renderer.domElement)
|
||||
@@ -163,60 +134,60 @@ const initThreeJS = () => {
|
||||
|
||||
//
|
||||
|
||||
// 添加光源 - 参考截图的柔和光照效果
|
||||
// 添加光源 - 高亮显示模型,无阴影效果
|
||||
const addLights = () => {
|
||||
// 环境光 - 提供基础照明,参考截图的柔和效果
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.6)
|
||||
// 强环境光 - 提供整体高亮照明
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0)
|
||||
scene.add(ambientLight)
|
||||
|
||||
// 主方向光 - 从左上角照射,模拟截图中的光照方向
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
directionalLight.position.set(5, 5, 3)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.shadow.mapSize.width = 2048
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
directionalLight.shadow.camera.near = 0.5
|
||||
directionalLight.shadow.camera.far = 50
|
||||
directionalLight.shadow.camera.left = -10
|
||||
directionalLight.shadow.camera.right = 10
|
||||
directionalLight.shadow.camera.top = 10
|
||||
directionalLight.shadow.camera.bottom = -10
|
||||
// 主方向光 - 从前上方照射,高亮度无阴影
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.8)
|
||||
directionalLight.position.set(5, 8, 5)
|
||||
directionalLight.castShadow = false
|
||||
scene.add(directionalLight)
|
||||
|
||||
// 补充光源 - 从右侧照射,提供更均匀的光照
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 0.4)
|
||||
fillLight.position.set(-3, 3, 3)
|
||||
// 补充光源 - 从左侧照射,填充光照
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
fillLight.position.set(-5, 4, 3)
|
||||
fillLight.castShadow = false
|
||||
scene.add(fillLight)
|
||||
|
||||
// 背光 - 增加轮廓,但强度较低
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.15)
|
||||
backLight.position.set(0, 2, -5)
|
||||
scene.add(backLight)
|
||||
// 背景光 - 从背后照射,增加轮廓高亮
|
||||
const rimLight = new THREE.DirectionalLight(0xffffff, 1.0)
|
||||
rimLight.position.set(0, 3, -5)
|
||||
rimLight.castShadow = false
|
||||
scene.add(rimLight)
|
||||
|
||||
// 顶部光源 - 增加顶部高亮
|
||||
const topLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
topLight.position.set(0, 10, 0)
|
||||
topLight.castShadow = false
|
||||
scene.add(topLight)
|
||||
}
|
||||
|
||||
// 添加地面网格 - 参考截图的深色背景和浅色网格线
|
||||
// 添加地面网格 - 简洁网格,无阴影
|
||||
const addGround = () => {
|
||||
// 创建网格辅助线 - 使用深色背景配浅色网格线,增加网格密度
|
||||
const gridHelper = new THREE.GridHelper(20, 40, 0x666666, 0x666666)
|
||||
gridHelper.position.y = -0.01 // 稍微向下一点,避免z-fighting
|
||||
// 创建网格辅助线 - 使用深色线条
|
||||
const gridHelper = new THREE.GridHelper(20, 20, 0x555555, 0x555555)
|
||||
gridHelper.position.y = 0
|
||||
scene.add(gridHelper)
|
||||
|
||||
// 添加半透明地面 - 使用更深的颜色
|
||||
// 简单透明地面平面
|
||||
const groundGeometry = new THREE.PlaneGeometry(20, 20)
|
||||
const groundMaterial = new THREE.MeshLambertMaterial({
|
||||
color: 0x1a1a1a, // 更深的背景色
|
||||
const groundMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0x404040,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
opacity: 0.1,
|
||||
})
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||
ground.rotation.x = -Math.PI / 2
|
||||
ground.receiveShadow = true
|
||||
ground.position.y = -0.01
|
||||
scene.add(ground)
|
||||
}
|
||||
|
||||
// 添加坐标轴辅助线 - 参考截图的样式
|
||||
// 添加坐标轴辅助线 - 匹配截图样式
|
||||
const addAxesHelper = () => {
|
||||
const axesHelper = new THREE.AxesHelper(3) // 稍微小一点的坐标轴
|
||||
const axesHelper = new THREE.AxesHelper(2)
|
||||
scene.add(axesHelper)
|
||||
}
|
||||
|
||||
@@ -244,7 +215,7 @@ const loadModel = async () => {
|
||||
|
||||
let loadedModel
|
||||
|
||||
switch (props.modelType.toLowerCase()) {
|
||||
switch (modelType.value) {
|
||||
case 'glb':
|
||||
case 'gltf':
|
||||
loadedModel = await loadGLTF(props.modelUrl)
|
||||
@@ -256,7 +227,7 @@ const loadModel = async () => {
|
||||
loadedModel = await loadSTL(props.modelUrl)
|
||||
break
|
||||
default:
|
||||
throw new Error(`不支持的模型格式: ${props.modelType}`)
|
||||
throw new Error(`不支持的模型格式: ${modelType.value}`)
|
||||
}
|
||||
|
||||
if (loadedModel) {
|
||||
@@ -276,13 +247,13 @@ const loadModel = async () => {
|
||||
baseScale = maxDim > 0 ? 2 / maxDim : 1
|
||||
|
||||
// 应用初始缩放
|
||||
model.scale.setScalar(baseScale * scale.value)
|
||||
model.scale.setScalar(baseScale)
|
||||
|
||||
// 根据模型大小调整相机距离 - 保持截图中的俯视角度
|
||||
const cameraDistance = maxDim > 0 ? maxDim * 2 : 5
|
||||
|
||||
// 设置相机位置为左上角俯视角度
|
||||
camera.position.set(cameraDistance * 0.6, cameraDistance * 0.6, cameraDistance * 0.6)
|
||||
// 设置相机位置 - 匹配截图中的正面稍俯视角度
|
||||
camera.position.set(cameraDistance * 0.3, cameraDistance * 0.4, cameraDistance * 1.2)
|
||||
camera.lookAt(0, 0, 0)
|
||||
|
||||
if (controls) {
|
||||
@@ -290,28 +261,14 @@ const loadModel = async () => {
|
||||
controls.update()
|
||||
}
|
||||
|
||||
// 设置阴影和材质
|
||||
// 移除阴影设置,让模型高亮显示
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
|
||||
// 将模型材质改为亮色
|
||||
child.castShadow = false
|
||||
child.receiveShadow = false
|
||||
// 如果材质支持,增加发光效果
|
||||
if (child.material) {
|
||||
const colorHex = modelColor.value.replace('#', '0x')
|
||||
// 如果是数组材质
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((mat) => {
|
||||
if (mat.color) {
|
||||
mat.color.setHex(colorHex)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 单个材质
|
||||
if (child.material.color) {
|
||||
child.material.color.setHex(colorHex)
|
||||
}
|
||||
}
|
||||
child.material.emissive = new THREE.Color(0x111111) // 轻微发光
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -403,51 +360,6 @@ const loadSTL = (url) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 放大
|
||||
const zoomIn = () => {
|
||||
if (scale.value < 3) {
|
||||
scale.value = Math.min(scale.value + 0.1, 3)
|
||||
updateScale(scale.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小
|
||||
const zoomOut = () => {
|
||||
if (scale.value > 0.1) {
|
||||
scale.value = Math.max(scale.value - 0.1, 0.1)
|
||||
updateScale(scale.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缩放
|
||||
const updateScale = (value) => {
|
||||
if (model) {
|
||||
model.scale.setScalar(baseScale * value)
|
||||
console.log('ThreeDPreview: 更新缩放', { value, baseScale, finalScale: baseScale * value })
|
||||
}
|
||||
}
|
||||
|
||||
// 更新模型颜色
|
||||
const updateModelColor = (color) => {
|
||||
if (model && color) {
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh && child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((mat) => {
|
||||
if (mat.color) {
|
||||
mat.color.setHex(color.replace('#', '0x'))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (child.material.color) {
|
||||
child.material.color.setHex(color.replace('#', '0x'))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// 重试加载
|
||||
@@ -460,8 +372,8 @@ const onWindowResize = () => {
|
||||
if (!container.value || !camera || !renderer) return
|
||||
|
||||
const containerRect = container.value.getBoundingClientRect()
|
||||
const width = Math.max(containerRect.width || 400, 400)
|
||||
const height = Math.max(containerRect.height || 300, 300)
|
||||
const width = containerRect.width || 400
|
||||
const height = containerRect.height || 300
|
||||
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
@@ -552,62 +464,24 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
background: #f0f0f0;
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
// 移除min-height限制,让高度完全自适应
|
||||
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.el-color-picker--small {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.scale-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.scale-value {
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
// 确保在弹窗中能正确填充
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,15 +559,4 @@ onUnmounted(() => {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.control-panel {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
margin: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,8 +15,11 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskList = ref([])
|
||||
const currentPreviewTask = ref(null)
|
||||
const currentPreviewTask = ref({
|
||||
downloading: false,
|
||||
})
|
||||
const giteeAdvancedVisible = ref(false)
|
||||
const taskPulling = ref(false)
|
||||
|
||||
const tencentDefaultForm = {
|
||||
text3d: false,
|
||||
@@ -49,6 +52,9 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
const tencentSupportedFormats = ref([])
|
||||
const giteeSupportedFormats = ref([])
|
||||
|
||||
// 定时器引用
|
||||
let taskPullHandler = null
|
||||
|
||||
const configs = ref({
|
||||
gitee: { models: [] },
|
||||
tencent: { models: [] },
|
||||
@@ -111,19 +117,9 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
try {
|
||||
loading.value = true
|
||||
requestData.type = activePlatform.value
|
||||
if (requestData.image_url !== '') {
|
||||
requestData.image_url = replaceImg(requestData.image_url[0].url)
|
||||
}
|
||||
const response = await httpPost('/api/ai3d/generate', requestData)
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('任务创建成功')
|
||||
tencentForm.value = { ...tencentDefaultForm }
|
||||
giteeForm.value = { ...giteeDefaultForm }
|
||||
currentPower.value = 0
|
||||
await loadTasks()
|
||||
} else {
|
||||
ElMessage.error(response.message || '创建任务失败')
|
||||
}
|
||||
ElMessage.success('任务创建成功')
|
||||
await loadTasks()
|
||||
} catch (error) {
|
||||
ElMessage.error('创建任务失败:' + error.message)
|
||||
} finally {
|
||||
@@ -133,12 +129,24 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const response = await httpGet('/api/ai3d/jobs/mock', {
|
||||
const response = await httpGet('/api/ai3d/jobs', {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
if (response.code === 0) {
|
||||
taskList.value = response.data.items
|
||||
let needPull = false
|
||||
const items = response.data.items
|
||||
|
||||
// 检查是否有进行中的任务
|
||||
for (let item of items) {
|
||||
if (item.status === 'pending' || item.status === 'processing') {
|
||||
needPull = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
taskPulling.value = needPull
|
||||
taskList.value = items
|
||||
total.value = response.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -222,16 +230,16 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '等待中',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
pending: { text: '等待中', type: 'warning' },
|
||||
processing: { text: '处理中', type: 'primary' },
|
||||
success: { text: '已完成', type: 'success' },
|
||||
failed: { text: '失败', type: 'danger' },
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getTaskCardClass = (status) => {
|
||||
if (status === 'completed') return 'task-card-completed'
|
||||
if (status === 'success') return 'task-card-completed'
|
||||
if (status === 'processing') return 'task-card-processing'
|
||||
if (status === 'failed') return 'task-card-failed'
|
||||
return 'task-card-default'
|
||||
@@ -249,30 +257,14 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
return '未知平台'
|
||||
}
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
if (status === 'pending') return 'iconfont icon-pending'
|
||||
if (status === 'processing') return 'iconfont icon-processing'
|
||||
if (status === 'completed') return 'iconfont icon-completed'
|
||||
if (status === 'failed') return 'iconfont icon-failed'
|
||||
return 'iconfont icon-question'
|
||||
}
|
||||
|
||||
const getTaskPrompt = (task) => {
|
||||
try {
|
||||
if (task.params) {
|
||||
const parsedParams = JSON.parse(task.params)
|
||||
return parsedParams.prompt || '文生3D任务'
|
||||
}
|
||||
return '文生3D任务'
|
||||
} catch (e) {
|
||||
return '文生3D任务'
|
||||
}
|
||||
return task.params.prompt ? task.params.prompt : '图生3D任务'
|
||||
}
|
||||
|
||||
const getTaskImageUrl = (task) => {
|
||||
try {
|
||||
if (task.params) {
|
||||
const parsedParams = JSON.parse(task.params)
|
||||
const parsedParams = task.params
|
||||
return parsedParams.image_url || null
|
||||
}
|
||||
return null
|
||||
@@ -282,25 +274,30 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
}
|
||||
|
||||
const getTaskParams = (task) => {
|
||||
try {
|
||||
if (task.params) {
|
||||
const parsedParams = JSON.parse(task.params)
|
||||
const params = []
|
||||
if (parsedParams.texture) params.push('纹理')
|
||||
if (parsedParams.enable_pbr) params.push('PBR材质')
|
||||
if (parsedParams.num_inference_steps && parsedParams.num_inference_steps !== 5)
|
||||
params.push(`迭代次数: ${parsedParams.num_inference_steps}`)
|
||||
if (parsedParams.guidance_scale && parsedParams.guidance_scale !== 7.5)
|
||||
params.push(`引导系数: ${parsedParams.guidance_scale}`)
|
||||
if (parsedParams.octree_resolution && parsedParams.octree_resolution !== 128)
|
||||
params.push(`精度: ${parsedParams.octree_resolution}`)
|
||||
if (parsedParams.seed && parsedParams.seed !== 1234)
|
||||
params.push(`种子: ${parsedParams.seed}`)
|
||||
return params.join(',')
|
||||
const parsedParams = task.params
|
||||
const params = []
|
||||
if (parsedParams.texture) params.push('纹理')
|
||||
if (parsedParams.enable_pbr) params.push('PBR材质')
|
||||
if (parsedParams.num_inference_steps)
|
||||
params.push(`迭代次数: ${parsedParams.num_inference_steps}`)
|
||||
if (parsedParams.guidance_scale) params.push(`引导系数: ${parsedParams.guidance_scale}`)
|
||||
if (parsedParams.octree_resolution) params.push(`精度: ${parsedParams.octree_resolution}`)
|
||||
if (parsedParams.seed) params.push(`Seed: ${parsedParams.seed}`)
|
||||
return params.join(',')
|
||||
}
|
||||
|
||||
const startTaskPolling = () => {
|
||||
taskPullHandler = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
loadTasks()
|
||||
}
|
||||
return ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const stopTaskPolling = () => {
|
||||
if (taskPullHandler) {
|
||||
clearInterval(taskPullHandler)
|
||||
taskPullHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +307,7 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
checkSession()
|
||||
.then(() => {
|
||||
loadTasks()
|
||||
startTaskPolling()
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
@@ -325,6 +323,7 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
taskList,
|
||||
currentPreviewTask,
|
||||
giteeAdvancedVisible,
|
||||
taskPulling,
|
||||
tencentForm,
|
||||
giteeForm,
|
||||
currentPower,
|
||||
@@ -353,9 +352,10 @@ export const useAI3DStore = defineStore('ai3d', () => {
|
||||
getTaskCardClass,
|
||||
getPlatformIcon,
|
||||
getPlatformName,
|
||||
getStatusIcon,
|
||||
getTaskPrompt,
|
||||
getTaskImageUrl,
|
||||
getTaskParams,
|
||||
startTaskPolling,
|
||||
stopTaskPolling,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -87,8 +87,8 @@
|
||||
<span class="label mb-3">随机种子:</span>
|
||||
<el-input-number
|
||||
v-model="giteeForm.seed"
|
||||
:min="1"
|
||||
:max="999999"
|
||||
:min="0"
|
||||
:max="10000000"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -278,12 +278,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-status-wrapper">
|
||||
<div class="task-status" :class="task.status">
|
||||
<i :class="getStatusIcon(task.status)" class="mr-1"></i>
|
||||
{{ getStatusText(task.status) }}
|
||||
<div class="task-status">
|
||||
<el-button
|
||||
size="small"
|
||||
:type="getStatusText(task.status).type"
|
||||
class="action-btn processing-btn"
|
||||
disabled
|
||||
round
|
||||
>
|
||||
<i
|
||||
class="iconfont icon-loading animate-spin mr-1"
|
||||
v-if="task.status === 'processing'"
|
||||
></i>
|
||||
{{ getStatusText(task.status).text }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="task-power">
|
||||
<i class="iconfont icon-suanli mr-1"></i>
|
||||
<i class="iconfont icon-power mr-1"></i>
|
||||
{{ task.power }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,49 +303,49 @@
|
||||
<!-- 任务卡片内容 -->
|
||||
<div class="task-card-content">
|
||||
<!-- 左侧预览图 -->
|
||||
<div class="task-preview">
|
||||
<div v-if="task.status === 'completed' && task.preview_url" class="preview-image">
|
||||
<div class="task-preview rounded-lg">
|
||||
<div v-if="task.status === 'success' && task.preview_url" class="preview-image">
|
||||
<img :src="task.preview_url" :alt="getTaskPrompt(task)" />
|
||||
<div class="preview-overlay">
|
||||
<i class="iconfont icon-yulan"></i>
|
||||
<div class="preview-overlay cursor-pointer" @click="preview3D(task)">
|
||||
<i class="iconfont icon-eye-open !text-3xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="getTaskImageUrl(task)" class="input-image">
|
||||
<img :src="getTaskImageUrl(task)" :alt="getTaskPrompt(task)" />
|
||||
<div class="input-overlay">
|
||||
<i class="iconfont icon-tupian"></i>
|
||||
<i class="iconfont icon-cube !text-3xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="prompt-placeholder">
|
||||
<i class="iconfont icon-wenzi"></i>
|
||||
<span>{{ getTaskPrompt(task) }}</span>
|
||||
<i class="iconfont icon-doc"></i>
|
||||
<span>文生3D任务</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧任务详情 -->
|
||||
<div class="task-details">
|
||||
<div class="task-model">
|
||||
<i class="iconfont icon-moxing mr-1"></i>
|
||||
<i class="iconfont icon-model !text-2xl mr-1"></i>
|
||||
{{ task.model }}
|
||||
</div>
|
||||
|
||||
<div class="task-prompt" v-if="getTaskPrompt(task)">
|
||||
<i class="iconfont icon-tishi mr-1"></i>
|
||||
<i class="iconfont icon-info !text-lg mr-1"></i>
|
||||
<span>{{ getTaskPrompt(task) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="task-params" v-if="getTaskParams(task)">
|
||||
<i class="iconfont icon-shezhi mr-1"></i>
|
||||
<i class="iconfont icon-tag !text-lg mr-1"></i>
|
||||
<span>{{ getTaskParams(task) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="task-time">
|
||||
<i class="iconfont icon-shijian mr-1"></i>
|
||||
<i class="iconfont icon-clock !text-xl mr-1"></i>
|
||||
{{ dateFormat(task.created_at) }}
|
||||
</div>
|
||||
|
||||
<div class="task-error" v-if="task.status === 'failed' && task.err_msg">
|
||||
<i class="iconfont icon-cuowu mr-1"></i>
|
||||
<i class="iconfont icon-error !text-base mr-1"></i>
|
||||
<span>{{ task.err_msg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,7 +355,7 @@
|
||||
<div class="task-card-footer">
|
||||
<div class="task-actions">
|
||||
<el-button
|
||||
v-if="task.status === 'completed'"
|
||||
v-if="task.status === 'success'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="preview3D(task)"
|
||||
@@ -355,7 +366,7 @@
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="task.status === 'completed'"
|
||||
v-if="task.status === 'success'"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="downloadFile(task)"
|
||||
@@ -376,17 +387,6 @@
|
||||
<i class="iconfont icon-remove mr-1"></i>
|
||||
删除
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="task.status === 'processing'"
|
||||
size="small"
|
||||
type="info"
|
||||
disabled
|
||||
class="action-btn processing-btn"
|
||||
>
|
||||
<i class="iconfont icon-loading animate-spin mr-1"></i>
|
||||
处理中...
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,7 +414,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 3D预览弹窗 -->
|
||||
<el-dialog v-model="previewVisible" title="3D模型预览" width="80%" :before-close="closePreview">
|
||||
<el-dialog v-model="previewVisible" title="3D模型预览" fullscreen :before-close="closePreview">
|
||||
<div class="preview-container">
|
||||
<ThreeDPreview
|
||||
v-if="currentPreviewTask && currentPreviewTask.file_url"
|
||||
@@ -487,7 +487,6 @@ const {
|
||||
getTaskCardClass,
|
||||
getPlatformIcon,
|
||||
getPlatformName,
|
||||
getStatusIcon,
|
||||
getTaskPrompt,
|
||||
getTaskImageUrl,
|
||||
getTaskParams,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
<el-input
|
||||
v-model="configs.tencent.secret_id"
|
||||
placeholder="请输入腾讯云SecretId"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@@ -30,7 +29,6 @@
|
||||
<el-input
|
||||
v-model="configs.tencent.secret_key"
|
||||
placeholder="请输入腾讯云SecretKey"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@@ -132,11 +130,7 @@
|
||||
<h4>秘钥配置</h4>
|
||||
<el-form :model="configs.gitee" label-width="140px" label-position="top">
|
||||
<el-form-item label="API密钥">
|
||||
<el-input
|
||||
v-model="configs.gitee.api_key"
|
||||
placeholder="请输入Gitee API密钥"
|
||||
show-password
|
||||
/>
|
||||
<el-input v-model="configs.gitee.api_key" placeholder="请输入Gitee API密钥" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="等待中" value="pending" />
|
||||
<el-option label="处理中" value="processing" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已完成" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -73,7 +73,7 @@
|
||||
<i class="iconfont icon-check"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.completed }}</div>
|
||||
<div class="stat-number">{{ stats.success }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,8 +94,8 @@
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="table-section w-full">
|
||||
<el-table :data="taskList" v-loading="loading" stripe border style="width: 100%">
|
||||
<el-table-column prop="user_id" label="用户ID" />
|
||||
<el-table :data="taskList" v-loading="loading" border style="width: 100%">
|
||||
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||
<el-table-column prop="type" label="平台">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'gitee' ? 'success' : 'primary'">
|
||||
@@ -103,7 +103,12 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="model" label="模型格式" />
|
||||
<el-table-column prop="model" label="模型名称" />
|
||||
<el-table-column label="模型格式">
|
||||
<template #default="{ row }">
|
||||
{{ row.params.file_format }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="power" label="算力消耗" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
@@ -114,17 +119,26 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.created_at) }}
|
||||
{{ dateFormat(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.updated_at) }}
|
||||
{{ dateFormat(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewTask(row)">查看</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
v-if="row.status === 'success'"
|
||||
@click="openModelPreview(row)"
|
||||
>
|
||||
预览模型
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteTask(row.id)"> 删除 </el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -160,7 +174,7 @@
|
||||
{{ currentTask.type === 'gitee' ? '魔力方舟' : '腾讯混元' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="模型格式">{{ currentTask.model }}</el-descriptions-item>
|
||||
<el-descriptions-item label="模型名称">{{ currentTask.model }}</el-descriptions-item>
|
||||
<el-descriptions-item label="算力消耗">{{ currentTask.power }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务状态">
|
||||
<el-tag :type="getStatusType(currentTask.status)">
|
||||
@@ -168,24 +182,31 @@
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{
|
||||
formatTime(currentTask.created_at)
|
||||
dateFormat(currentTask.created_at)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{
|
||||
formatTime(currentTask.updated_at)
|
||||
dateFormat(currentTask.updated_at)
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="task-params">
|
||||
<h4>任务参数</h4>
|
||||
<el-input v-model="taskParamsDisplay" type="textarea" :rows="6" readonly />
|
||||
<div class="params-content">
|
||||
<pre>{{ JSON.stringify(currentTask.params, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentTask.img_url" class="task-result">
|
||||
<div v-if="currentTask.img_url || currentTask.file_url" class="task-result">
|
||||
<h4>生成结果</h4>
|
||||
<div class="result-links">
|
||||
<el-button type="primary" @click="downloadModel(currentTask)"> 下载3D模型 </el-button>
|
||||
<el-button v-if="currentTask.preview_url" @click="viewPreview(currentTask.preview_url)">
|
||||
查看预览
|
||||
<el-button
|
||||
v-if="currentTask.file_url"
|
||||
type="success"
|
||||
plain
|
||||
@click="openModelPreview(currentTask)"
|
||||
>
|
||||
预览模型
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,19 +224,40 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 预览图片弹窗 -->
|
||||
<el-dialog v-model="previewVisible" title="预览图片" width="50%">
|
||||
<div class="preview-container">
|
||||
<el-image :src="previewUrl" fit="contain" style="width: 100%; height: 400px" />
|
||||
<!-- 3D 模型预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="modelPreviewVisible"
|
||||
:class="['model-preview-dialog', { dark: isDarkTheme }]"
|
||||
title="模型预览"
|
||||
fullscreen
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="model-preview-wrapper">
|
||||
<ThreeDPreview :model-url="modelPreviewUrl" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="downloadModel(currentTask)"
|
||||
:loading="currentTask.downloading"
|
||||
>
|
||||
下载3D模型
|
||||
</el-button>
|
||||
<el-button @click="modelPreviewVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet } from '@/utils/http'
|
||||
import ThreeDPreview from '@/components/ThreeDPreview.vue'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet } from '@/utils/http'
|
||||
import { dateFormat, replaceImg } from '@/utils/libs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -224,9 +266,17 @@ const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const taskList = ref([])
|
||||
const taskDetailVisible = ref(false)
|
||||
const previewVisible = ref(false)
|
||||
const currentTask = ref(null)
|
||||
const currentTask = ref({
|
||||
downloading: false,
|
||||
})
|
||||
const previewUrl = ref('')
|
||||
// 3D 预览
|
||||
const modelPreviewVisible = ref(false)
|
||||
const modelPreviewUrl = ref('')
|
||||
// 简单检测暗色主题(若全局有主题管理可替换)
|
||||
const isDarkTheme = ref(
|
||||
document.documentElement.classList.contains('dark') || document.body.classList.contains('dark')
|
||||
)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
@@ -243,18 +293,6 @@ const stats = reactive({
|
||||
failed: 0,
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const taskParamsDisplay = computed(() => {
|
||||
if (!currentTask.value?.params) return '无参数'
|
||||
|
||||
try {
|
||||
const params = JSON.parse(currentTask.value.params)
|
||||
return JSON.stringify(params, null, 2)
|
||||
} catch {
|
||||
return currentTask.value.params
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadData = async () => {
|
||||
try {
|
||||
@@ -276,7 +314,7 @@ const loadData = async () => {
|
||||
const response = await httpGet('/api/admin/ai3d/jobs', params)
|
||||
|
||||
if (response.code === 0) {
|
||||
taskList.value = response.data.list
|
||||
taskList.value = response.data.items
|
||||
total.value = response.data.total
|
||||
} else {
|
||||
ElMessage.error(response.message || '加载数据失败')
|
||||
@@ -364,31 +402,46 @@ const deleteTask = async (taskId) => {
|
||||
}
|
||||
}
|
||||
|
||||
const downloadModel = (task) => {
|
||||
if (task.img_url) {
|
||||
const downloadModel = async (task) => {
|
||||
const url = replaceImg(task.file_url)
|
||||
const downloadURL = `/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
task.downloading = true
|
||||
try {
|
||||
const response = await httpDownload(downloadURL)
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = task.img_url
|
||||
link.download = `3d_model_${task.id}.${task.model}`
|
||||
link.style.display = 'none'
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
ElMessage.success('开始下载3D模型')
|
||||
} else {
|
||||
ElMessage.warning('模型文件不存在')
|
||||
URL.revokeObjectURL(link.href)
|
||||
task.downloading = false
|
||||
} catch (error) {
|
||||
showMessageError('下载失败:' + error.message)
|
||||
task.downloading = false
|
||||
}
|
||||
}
|
||||
|
||||
const viewPreview = (url) => {
|
||||
previewUrl.value = url
|
||||
previewVisible.value = true
|
||||
const openModelPreview = (task) => {
|
||||
// 优先使用文件直链,后端下载代理也可拼接
|
||||
const url = task.file_url
|
||||
if (!url) {
|
||||
ElMessage.warning('暂无可预览的模型文件')
|
||||
return
|
||||
}
|
||||
currentTask.value = task
|
||||
modelPreviewUrl.value = url
|
||||
modelPreviewVisible.value = true
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
pending: 'warning',
|
||||
processing: 'primary',
|
||||
completed: 'success',
|
||||
success: 'success',
|
||||
failed: 'danger',
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
@@ -398,24 +451,12 @@ const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
pending: '等待中',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
success: '已完成',
|
||||
failed: '失败',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
const getProgressStatus = (status) => {
|
||||
if (status === 'failed') return 'exception'
|
||||
if (status === 'completed') return 'success'
|
||||
return ''
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp * 1000)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
@@ -424,128 +465,5 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.admin-threed-jobs {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
.el-select__wrapper {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.processing {
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
background: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-detail {
|
||||
.task-params,
|
||||
.task-result,
|
||||
.task-error {
|
||||
margin-top: 20px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-links {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
@use '@/assets/css/admin/ai3d.scss' as *;
|
||||
</style>
|
||||
|
||||
@@ -4,62 +4,56 @@
|
||||
<el-form
|
||||
:model="jimengConfig"
|
||||
label-width="150px"
|
||||
label-position="right"
|
||||
label-position="top"
|
||||
ref="configFormRef"
|
||||
:rules="rules"
|
||||
class="py-3 px-5"
|
||||
>
|
||||
<!-- 秘钥配置分组 -->
|
||||
<div class="mb-3">
|
||||
<h3 class="mb-2">秘钥配置</h3>
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<p class="mb-1">
|
||||
1. 要使用即梦 AI 功能,需要先在火山引擎控制台开通
|
||||
<a
|
||||
href="https://console.volcengine.com/ai/ability/detail/10"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>即梦 AI</a
|
||||
>
|
||||
和
|
||||
<a
|
||||
href="https://console.volcengine.com/ai/ability/detail/9"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>智能绘图</a
|
||||
>
|
||||
服务。
|
||||
</p>
|
||||
<p>
|
||||
2. AccessKey和SecretKey 请在火山引擎控制台 ->
|
||||
<a
|
||||
href="https://console.volcengine.com/iam/keymanage/"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>秘钥管理</a
|
||||
>
|
||||
获取。
|
||||
</p>
|
||||
</el-alert>
|
||||
<h3 class="heading-3 mb-2">秘钥配置</h3>
|
||||
<div class="py-3">
|
||||
<Alert type="info">
|
||||
<p class="mb-1">
|
||||
1. 要使用即梦 AI 功能,需要先在火山引擎控制台开通
|
||||
<a
|
||||
href="https://console.volcengine.com/ai/ability/detail/10"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>即梦 AI</a
|
||||
>
|
||||
和
|
||||
<a
|
||||
href="https://console.volcengine.com/ai/ability/detail/9"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>智能绘图</a
|
||||
>
|
||||
服务。
|
||||
</p>
|
||||
<p>
|
||||
2. AccessKey和SecretKey 请在火山引擎控制台 ->
|
||||
<a
|
||||
href="https://console.volcengine.com/iam/keymanage/"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>秘钥管理</a
|
||||
>
|
||||
获取。
|
||||
</p>
|
||||
</Alert>
|
||||
</div>
|
||||
<el-form-item label="AccessKey" prop="access_key">
|
||||
<el-input
|
||||
v-model="jimengConfig.access_key"
|
||||
placeholder="请输入即梦AI的AccessKey"
|
||||
show-password
|
||||
/>
|
||||
<el-input v-model="jimengConfig.access_key" placeholder="请输入即梦AI的AccessKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="SecretKey" prop="secret_key">
|
||||
<el-input
|
||||
v-model="jimengConfig.secret_key"
|
||||
placeholder="请输入即梦AI的SecretKey"
|
||||
show-password
|
||||
/>
|
||||
<el-input v-model="jimengConfig.secret_key" placeholder="请输入即梦AI的SecretKey" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-divider />
|
||||
<!-- 算力配置分组 -->
|
||||
<div class="mb-3">
|
||||
<h3 class="mb-3">算力配置</h3>
|
||||
<h3 class="heading-3 mb-3">算力配置</h3>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
@@ -205,6 +199,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -297,6 +292,10 @@ const resetConfig = () => {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.heading-3 {
|
||||
color: var(--theme-text-color-primary);
|
||||
}
|
||||
|
||||
.label-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
:data="taskList"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
border
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<p class="text-sm text-gray-500 mt-2">检测结果仅供参考</p>
|
||||
</div>
|
||||
|
||||
<el-table :data="testResult.details" border stripe class="result-table">
|
||||
<el-table :data="testResult.details" border class="result-table">
|
||||
<el-table-column prop="category" label="类别" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="font-medium">{{ row.category }}</span>
|
||||
|
||||
@@ -83,7 +83,6 @@
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user