diff --git a/web/src/assets/css/ai3d.scss b/web/src/assets/css/ai3d.scss index fa589488..c91ed909 100644 --- a/web/src/assets/css/ai3d.scss +++ b/web/src/assets/css/ai3d.scss @@ -1,13 +1,13 @@ .page-threed { display: flex; height: 100vh; - background: #f5f5f5; + background: var(--theme-bg-all); } .params-panel { width: 400px; - background: white; - border-right: 1px solid #e4e7ed; + background: var(--card-bg); + border-right: 1px solid var(--line-box); padding: 20px; overflow-y: auto; } @@ -23,7 +23,7 @@ i { font-size: 18px; - color: #409eff; + color: var(--text-color-primary); } } @@ -38,14 +38,14 @@ .label { display: block; font-weight: 600; - color: #414141; + color: var(--theme-text-color-primary); } } .advanced-toggle-btn { padding: 0; font-size: 14px; - color: #409eff; + color: var(--text-color-primary); border: none; background: none; display: flex; @@ -53,8 +53,8 @@ gap: 4px; &:hover { - color: #66b1ff; - background: #f0f9ff; + color: var(--text-color-primary); + background: var(--el-color-primary-light-9); border-radius: 4px; } } @@ -62,7 +62,7 @@ .advanced-params { padding: 10px 16px; border-radius: 8px; - border-left: 4px solid #2196f3; + border-left: 4px solid var(--text-color-primary); } } @@ -74,11 +74,11 @@ .power-value { font-size: 24px; font-weight: bold; - color: #409eff; + color: var(--text-color-primary); } .power-unit { - color: #666; + color: var(--theme-text-color-secondary); } } @@ -105,13 +105,13 @@ align-items: center; margin-bottom: 30px; padding: 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--panel-bg); border-radius: 12px; - color: white; + color: var(--theme-text-color-primary); h3 { margin: 0; - color: white; + color: var(--theme-text-color-primary); font-size: 24px; font-weight: 600; display: flex; @@ -121,32 +121,32 @@ content: ''; width: 4px; height: 24px; - background: white; + background: var(--theme-text-color-primary); margin-right: 12px; border-radius: 2px; } } .el-button { - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - color: white; + // background: rgba(255, 255, 255, 0.2); + // border: 1px solid rgba(255, 255, 255, 0.3); + // color: white; - &:hover { - background: rgba(255, 255, 255, 0.3); - border-color: rgba(255, 255, 255, 0.5); - } + // &:hover { + // background: rgba(255, 255, 255, 0.3); + // border-color: rgba(255, 255, 255, 0.5); + // } } } } .task-items { .task-card { - background: white; + background: var(--card-bg); border-radius: 12px; padding: 16px; margin-bottom: 16px; - border: 1px solid #e4e7ed; + border: 1px solid var(--line-box); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; @@ -161,22 +161,22 @@ &.task-card-completed { border-left: 4px solid #67c23a; - background: #f0f9eb; + background: var(--el-fill-color-light); } &.task-card-processing { - border-left: 4px solid #409eff; - background: #ecf5ff; + border-left: 4px solid var(--text-color-primary); + background: var(--el-fill-color-light); } &.task-card-failed { border-left: 4px solid #f56c6c; - background: #fef0f0; + background: var(--el-fill-color-light); } &.task-card-default { border-left: 4px solid #909399; - background: #f4f4f4; + background: var(--el-fill-color-light); } } @@ -196,25 +196,25 @@ .task-id { font-size: 18px; font-weight: bold; - color: #333; + color: var(--theme-text-color-primary); display: flex; align-items: center; i { font-size: 20px; - color: #409eff; + color: var(--text-color-primary); } } .task-platform { font-size: 14px; - color: #666; + color: var(--theme-text-color-secondary); display: flex; align-items: center; i { font-size: 16px; - color: #409eff; + color: var(--text-color-primary); } } } @@ -233,22 +233,22 @@ align-items: center; &.pending { - background: #fffbe6; + background: var(--el-fill-color-light); color: #e6a23c; } &.processing { - background: #e1f3d8; + background: var(--el-fill-color-light); color: #67c23a; } &.completed { - background: #e1f3d8; + background: var(--el-fill-color-light); color: #67c23a; } &.failed { - background: #fef0f0; + background: var(--el-fill-color-light); color: #f56c6c; } @@ -260,14 +260,14 @@ .task-power { font-size: 14px; - color: #666; + color: var(--theme-text-color-secondary); display: flex; align-items: center; i { font-size: 14px; margin-right: 4px; - color: #409eff; + color: var(--text-color-primary); } } } @@ -283,11 +283,11 @@ position: relative; border-radius: 8px; overflow: hidden; - background: #f0f0f0; + background: var(--chat-wel-bg); display: flex; align-items: center; justify-content: center; - color: #666; + color: var(--theme-text-color-secondary); min-height: 120px; max-width: 200px; @@ -313,7 +313,7 @@ display: flex; align-items: center; justify-content: center; - color: white; + color: var(--theme-text-color-primary); font-size: 24px; opacity: 0; transition: opacity 0.3s ease; @@ -363,7 +363,7 @@ flex-direction: column; align-items: center; justify-content: center; - color: #999; + color: var(--theme-text-color-secondary); i { font-size: 48px; @@ -385,20 +385,20 @@ .task-model { font-size: 16px; font-weight: bold; - color: #333; + color: var(--theme-text-color-primary); display: flex; align-items: center; i { font-size: 18px; margin-right: 6px; - color: #409eff; + color: var(--text-color-primary); } } .task-prompt { font-size: 14px; - color: #666; + color: var(--theme-text-color-secondary); display: flex; align-items: center; margin-top: 4px; @@ -406,13 +406,13 @@ i { font-size: 14px; margin-right: 6px; - color: #909399; + color: var(--theme-text-color-secondary); } } .task-params { font-size: 14px; - color: #666; + color: var(--theme-text-color-secondary); display: flex; align-items: center; margin-top: 4px; @@ -420,13 +420,13 @@ i { font-size: 14px; margin-right: 6px; - color: #909399; + color: var(--theme-text-color-secondary); } } .task-time { font-size: 12px; - color: #999; + color: var(--theme-text-color-secondary); display: flex; align-items: center; margin-top: 4px; @@ -457,7 +457,7 @@ justify-content: flex-end; gap: 8px; padding-top: 12px; - border-top: 1px dashed #eee; + border-top: 1px dashed var(--line-box); .task-actions { display: flex; @@ -473,19 +473,19 @@ gap: 4px; &.preview-btn { - background: #409eff; - color: white; - border: 1px solid #409eff; + background: var(--text-color-primary); + // color: var(--theme-text-color-primary); + border: 1px solid var(--text-color-primary); &:hover { - background: #66b1ff; - border-color: #66b1ff; + background: var(--text-color-primary); + border-color: var(--border-active); } } &.download-btn { background: #67c23a; - color: white; + // color: var(--theme-text-color-primary); border: 1px solid #67c23a; &:hover { @@ -496,7 +496,7 @@ &.delete-btn { background: #f56c6c; - color: white; + // color: var(--theme-text-color-primary); border: 1px solid #f56c6c; &:hover { @@ -507,7 +507,7 @@ &.processing-btn { background: #909399; - color: white; + // color: var(--theme-text-color-primary); border: 1px solid #909399; cursor: not-allowed; } @@ -518,13 +518,13 @@ .empty-state { width: 100%; height: 200px; - background: #f0f0f0; + background: var(--chat-wel-bg); border-radius: 12px; display: flex; flex-direction: column; align-items: center; justify-content: center; - color: #999; + color: var(--theme-text-color-secondary); font-size: 14px; i { @@ -544,36 +544,36 @@ width: 100%; height: 500px; min-height: 500px; - background: #f0f0f0; + background: var(--chat-wel-bg); border-radius: 8px; position: relative; .three-container { width: 100%; height: 500px; - background: #f0f0f0; + background: var(--chat-wel-bg); border-radius: 8px; display: flex; align-items: center; justify-content: center; - color: #666; + color: var(--theme-text-color-secondary); } .preview-placeholder { width: 100%; height: 500px; - background: #f0f0f0; + background: var(--chat-wel-bg); border-radius: 8px; display: flex; flex-direction: column; align-items: center; justify-content: center; - color: #666; + color: var(--theme-text-color-secondary); i { font-size: 48px; margin-bottom: 12px; - color: #999; + color: var(--theme-text-color-secondary); } p { @@ -591,7 +591,7 @@ .params-panel { width: 100%; border-right: none; - border-bottom: 1px solid #e4e7ed; + border-bottom: 1px solid var(--line-box); } .task-card-content { diff --git a/web/src/store/ai3d.js b/web/src/store/ai3d.js new file mode 100644 index 00000000..fe2bc6ff --- /dev/null +++ b/web/src/store/ai3d.js @@ -0,0 +1,361 @@ +import { checkSession } from '@/store/cache' +import { showMessageError } from '@/utils/dialog' +import { httpDownload, httpGet, httpPost } from '@/utils/http' +import { replaceImg } from '@/utils/libs' +import { ElMessage, ElMessageBox } from 'element-plus' +import { defineStore } from 'pinia' +import { computed, onMounted, ref } from 'vue' + +export const useAI3DStore = defineStore('ai3d', () => { + // 响应式数据 + const activePlatform = ref('gitee') + const loading = ref(false) + const previewVisible = ref(false) + const currentPage = ref(1) + const pageSize = ref(10) + const total = ref(0) + const taskList = ref([]) + const currentPreviewTask = ref(null) + const giteeAdvancedVisible = ref(false) + + const tencentDefaultForm = { + text3d: false, + prompt: '', + image_url: '', + model: '', + file_format: '', + enable_pbr: false, + model_desc: '', + power: 0, + } + + const giteeDefaultForm = { + prompt: '', + image_url: '', + model: '', + file_format: '', + texture: false, + seed: 1234, + num_inference_steps: 5, + guidance_scale: 7.5, + octree_resolution: 128, + model_desc: '', + power: 0, + } + + const tencentForm = ref({ ...tencentDefaultForm }) + const giteeForm = ref({ ...giteeDefaultForm }) + const currentPower = ref(0) + const tencentSupportedFormats = ref([]) + const giteeSupportedFormats = ref([]) + + const configs = ref({ + gitee: { models: [] }, + tencent: { models: [] }, + }) + + // 计算属性 + const currentForm = computed(() => + activePlatform.value === 'tencent' ? tencentForm.value : giteeForm.value + ) + const selectedModel = computed(() => currentForm.value.model) + const currentPrompt = computed(() => currentForm.value.prompt) + const currentImage = computed(() => + currentForm.value.image_url ? [{ url: currentForm.value.image_url }] : [] + ) + + // 加载配置 + const loadConfigs = async () => { + const response = await httpGet('/api/ai3d/configs') + configs.value = response.data + } + + const handleModelChange = (value) => { + if (activePlatform.value === 'tencent') { + const model = configs.value.tencent.models.find((m) => m.name === value) + if (!model) return + currentPower.value = model.power + tencentForm.value.power = model.power + tencentForm.value.model_desc = model.desc + tencentForm.value.file_format = model.formats[0] + tencentSupportedFormats.value = model.formats + } else { + const model = configs.value.gitee.models.find((m) => m.name === value) + if (!model) return + currentPower.value = model.power + giteeForm.value.power = model.power + giteeForm.value.model_desc = model.desc + giteeForm.value.file_format = model.formats[0] + giteeSupportedFormats.value = model.formats + } + } + + const handlePlatformChange = (value) => { + activePlatform.value = value + currentPower.value = value === 'tencent' ? tencentForm.value.power : giteeForm.value.power + } + + const generate3D = async () => { + const requestData = { + ...(activePlatform.value === 'tencent' ? tencentForm.value : giteeForm.value), + } + if (requestData.model === '') { + ElMessage.warning('请选择模型') + return + } + if (requestData.file_format === '') { + ElMessage.warning('请选择输出格式') + return + } + + 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 || '创建任务失败') + } + } catch (error) { + ElMessage.error('创建任务失败:' + error.message) + } finally { + loading.value = false + } + } + + const loadTasks = async () => { + try { + const response = await httpGet('/api/ai3d/jobs/mock', { + page: currentPage.value, + page_size: pageSize.value, + }) + if (response.code === 0) { + taskList.value = response.data.items + total.value = response.data.total + } + } catch (error) { + ElMessage.error('加载任务列表失败:' + error.message) + } + } + + const refreshTasks = () => { + loadTasks() + } + + const handlePageSizeChange = (size) => { + pageSize.value = size + currentPage.value = 1 + loadTasks() + } + + const handleCurrentPageChange = (page) => { + currentPage.value = page + loadTasks() + } + + const deleteTask = async (taskId) => { + try { + await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }) + const response = await httpGet(`/api/ai3d/job/delete?id=${taskId}`) + if (response.code === 0) { + ElMessage.success('删除成功') + loadTasks() + } else { + ElMessage.error(response.message || '删除失败') + } + } catch (error) { + if (error !== 'cancel') { + ElMessage.error('删除失败:' + error.message) + } + } + } + + const preview3D = (task) => { + currentPreviewTask.value = task + previewVisible.value = true + } + + const closePreview = () => { + previewVisible.value = false + } + + const downloadFile = async (item) => { + const url = replaceImg(item.file_url) + const downloadURL = `/api/download?url=${url}` + const urlObj = new URL(url) + const fileName = urlObj.pathname.split('/').pop() + item.downloading = true + try { + const response = await httpDownload(downloadURL) + const blob = new Blob([response.data]) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = fileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(link.href) + item.downloading = false + } catch (error) { + showMessageError('下载失败') + item.downloading = false + } + } + + const downloadCurrentModel = () => { + if (currentPreviewTask.value) { + downloadFile(currentPreviewTask.value) + } + } + + const getStatusText = (status) => { + const statusMap = { + pending: '等待中', + processing: '处理中', + completed: '已完成', + failed: '失败', + } + return statusMap[status] || status + } + + const getTaskCardClass = (status) => { + if (status === 'completed') return 'task-card-completed' + if (status === 'processing') return 'task-card-processing' + if (status === 'failed') return 'task-card-failed' + return 'task-card-default' + } + + const getPlatformIcon = (type) => { + if (type === 'gitee') return 'iconfont icon-gitee' + if (type === 'tencent') return 'iconfont icon-tencent' + return 'iconfont icon-question' + } + + const getPlatformName = (type) => { + if (type === 'gitee') return 'Gitee 模力方舟' + if (type === 'tencent') return '腾讯云混元3D' + 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任务' + } + } + + const getTaskImageUrl = (task) => { + try { + if (task.params) { + const parsedParams = JSON.parse(task.params) + return parsedParams.image_url || null + } + return null + } catch (e) { + return null + } + } + + 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(',') + } + return '' + } catch (e) { + return '' + } + } + + // 生命周期:加载配置与任务 + onMounted(() => { + loadConfigs() + checkSession() + .then(() => { + loadTasks() + }) + .catch(() => {}) + }) + + return { + // 状态 + activePlatform, + loading, + previewVisible, + currentPage, + pageSize, + total, + taskList, + currentPreviewTask, + giteeAdvancedVisible, + tencentForm, + giteeForm, + currentPower, + tencentSupportedFormats, + giteeSupportedFormats, + configs, + currentForm, + selectedModel, + currentPrompt, + currentImage, + // 方法 + loadConfigs, + handleModelChange, + handlePlatformChange, + generate3D, + loadTasks, + refreshTasks, + handlePageSizeChange, + handleCurrentPageChange, + deleteTask, + preview3D, + closePreview, + downloadFile, + downloadCurrentModel, + getStatusText, + getTaskCardClass, + getPlatformIcon, + getPlatformName, + getStatusIcon, + getTaskPrompt, + getTaskImageUrl, + getTaskParams, + } +}) diff --git a/web/src/views/AIThreeDCreate.vue b/web/src/views/AIThreeDCreate.vue index 9215659a..9d4d0a98 100644 --- a/web/src/views/AIThreeDCreate.vue +++ b/web/src/views/AIThreeDCreate.vue @@ -330,7 +330,7 @@