This commit is contained in:
13121765685 2025-12-08 14:59:11 +08:00
parent 79630d2802
commit 9f7047fbb9
25 changed files with 345 additions and 443 deletions

View File

@ -70,6 +70,10 @@ import { MeshyServer } from '@deotaland/utils'
const meshServer = new MeshyServer()
const isLoading = ref(false);
const props = defineProps({
resultTask: {
type: String,
required: true
},
taskId: {
type: String,
required: true
@ -150,24 +154,27 @@ watch(() => initializationProgress.value, (newVal) => {
})
const loadmodelUrl = ()=>{
if(props.taskId){
startModelTask(props.taskId)
startModelTask(props.taskId,props.resultTask)
}
else if(props.imgUrl){
meshServer.createModelTask({
image_url:props.imgUrl,
project_id:props.projectId
},(result)=>{
emit('save',result)
startModelTask(result)
},(result,resultTask)=>{
emit('save',result,resultTask)
startModelTask(result,resultTask)
},()=>{
emit('delete')
},{
})
}
}
const startModelTask = (result) => {
const startModelTask = (result,resultTask) => {
showInitializationOverlay.value = true;
meshServer.getModelTaskStatus(result,(modelurl)=>{
meshServer.getModelTaskStatus({
result:result,
resultTask:resultTask,
},(modelurl)=>{
setInitializationProgress(100)
modelUrl.value = modelurl;
},(error)=>{

View File

@ -160,10 +160,11 @@
@preview="previewModel"
:img-url="item.image"
:task-id="item.taskId"
:result-task="item.resultTask"
width="100%"
height="150px"
:project-id="orderDetail.projectId"
@save="(result)=>saveModel(index,result)"
@save="(result,resultTask)=>saveModel(index,result,resultTask)"
:show-delete="true"
@delete="deleteModel(index)"
/>
@ -312,9 +313,9 @@ const orderDetail = ref({
status: 'pending',
processingCompleteTime: null
})
const saveModel = (index,result)=>{
console.log(index,result,'currentStepcurrentStep');
const saveModel = (index,result,resultTask)=>{
generatedModels.value[index].taskId = result;
generatedModels.value[index].resultTask = resultTask;
}
//
@ -485,20 +486,7 @@ const handleDisassembly = () => {
const deleteModel = (index) => {
ElMessageBox.confirm(
'确定要删除这个模型吗?',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
generatedModels.value.splice(index, 1)
ElMessage.success('模型已删除')
}).catch(() => {
//
})
}
//
const handleProcessingComplete = () => {

View File

@ -26,6 +26,7 @@
"element-plus": "^2.11.7",
"jose": "^6.1.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"three": "^0.180.0",

View File

@ -18,7 +18,6 @@ function initializeTheme() {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme')
const hasDarkClass = document.documentElement.classList.contains('dark')
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark')
themeState.value = true
@ -31,7 +30,22 @@ function initializeTheme() {
}
}
}
const loading = ref(false);
const qmLoading = ref(false);
const closeMethods = {
close: ()=>{
loading.value = false
qmLoading.value = false;
}
}
window.setElLoading = (qp=false)=>{
if(qp){
qmLoading.value = true
}else{
loading.value = true
}
return closeMethods
}
// Initialize on component mount
onMounted(() => {
initializeTheme()
@ -44,41 +58,119 @@ onMounted(() => {
'fullscreen-mode': isFullScreenPage,
'homepage-mode': isHomePage
}">
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': qmLoading }"></div>
<!-- 登录页面全屏显示 -->
<main v-if="isLoginPage">
<main style="position: relative;" v-if="isLoginPage">
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div>
<router-view />
</main>
<!-- 全屏页面如创建项目 -->
<main v-else-if="isFullScreenPage" class="fullscreen-content">
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div>
<router-view />
</main>
<!-- 应用内页面使用布局组件 -->
<MainLayout v-else>
<div v-else style="position: relative;">
<MainLayout :loading="loading" >
<template #header>
<AppHeader />
</template>
<template #sidebar>
<AppSidebar />
</template>
<template #default>
<router-view />
</template>
</MainLayout>
</div>
</div>
<div v-else>
<router-view />
</div>
</template>
<style>
#nprogress .bar {
/* 自定义加载条颜色 */
background: #9c7eef !important; /* Element Plus 主色 */
height: 2px !important; /* 高度 */
border-radius: 0 !important;
}
/* 侧边栏过渡动画蒙层 */
.sidebar-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background: rgba(111, 75, 197, 0.1); */
/* background: red; */
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
/* 蒙层激活状态 */
.sidebar-overlay-active {
opacity: 1;
visibility: visible;
}
/* 转圈加载动画 */
.sidebar-overlay::before {
content: '';
width: 50px;
height: 50px;
border: 4px solid rgba(113, 77, 199, 0.3);
border-top: 4px solid #714DC7;
border-radius: 50%;
animation: spin 1s linear infinite;
box-shadow: 0 0 20px rgba(113, 77, 199, 0.5);
}
.sidebar-overlay::after {
content: '';
position: absolute;
width: 70px;
height: 70px;
border: 2px solid rgba(113, 77, 199, 0.1);
border-bottom: 2px solid rgba(113, 77, 199, 0.3);
border-radius: 50%;
animation: spin 1.5s linear infinite reverse;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 暗色主题下的蒙层效果 */
html.dark .sidebar-overlay {
background: rgba(31, 41, 55, 0.85);
}
html.dark .sidebar-overlay::before {
border: 4px solid rgba(255, 255, 255, 0.2);
border-top: 4px solid #A78BFA;
box-shadow: 0 0 20px rgba(167, 139, 250, 0.5);
}
html.dark .sidebar-overlay::after {
border: 2px solid rgba(167, 139, 250, 0.1);
border-bottom: 2px solid rgba(167, 139, 250, 0.4);
}
</style>
<style scoped>
header strong { font-size: 1.25rem; }
/* 应用容器基础样式 */
#app-container {
width: 100vw;
@ -119,7 +211,7 @@ header strong { font-size: 1.25rem; }
height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
overflow: auto !important; /* 确保全屏内容可以滚动 */
position: relative;
}
/* 首页全屏滚动模式 - 使用浏览器原生滚动 */

View File

@ -288,7 +288,7 @@ const handleGenerateImage = async () => {
formData.value.status = 'success';
saveProject();
} catch (error) {
ElMessage.error('生成图片失败,请稍后重试');
// ElMessage.error('');
emit('delete');
}
};

View File

@ -282,7 +282,7 @@ const formData = ref({
const textareaRef = ref(null);
const fileInput = ref(null); //
const generateCount = ref(1); // 1
const generateCount = ref(4); // 1
const isOptimizing = ref(false); //
const isDragOver = ref(false); //
// IP/

View File

@ -62,7 +62,7 @@
<el-dropdown-item command="settings">
{{ t('header.settings') }}
</el-dropdown-item> -->
<el-dropdown-item divided command="logout">
<el-dropdown-item command="logout">
<LogoutIcon class="dropdown-item-icon" />
{{ t('header.logout') }}
</el-dropdown-item>
@ -545,7 +545,7 @@ export default {
font-weight: 500;
color: var(--text-primary, #1f2937);
max-width: 100px;
overflow: hidden;
/* overflow: hidden; */
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -476,7 +476,7 @@ export default {
font-weight: 600;
color: var(--text-primary, #1f2937);
white-space: nowrap;
overflow: hidden;
/* overflow: hidden; */
text-overflow: ellipsis;
letter-spacing: 0.02em;
line-height: 1.3;

View File

@ -1,11 +1,5 @@
<template>
<div class="main-layout" :class="layoutClasses">
<!-- 移动端遮罩层 -->
<div
v-if="isMobile && sidebarVisible"
class="sidebar-overlay"
@click="toggleSidebar"
></div>
<!-- 第一行头部导航栏 -->
<header class="header-container">
@ -28,11 +22,13 @@
</aside>
<!-- 主内容区域 -->
<div class="main-content" :class="{ 'sidebar-collapsed': !sidebarVisible && !isMobile }">
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div>
<!-- 面包屑导航 -->
<!-- <BreadcrumbNavigation class="breadcrumb-container" /> -->
<!-- 页面内容 -->
<main class="page-content">
<router-view v-slot="{ Component, route }">
<keep-alive v-if="route.meta.keepAlive">
<component :is="Component" :key="route.name" />
@ -45,115 +41,109 @@
</div>
</template>
<script>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import AppHeader from './AppHeader.vue'
import AppSidebar from './AppSidebar.vue'
import BreadcrumbNavigation from './BreadcrumbNavigation.vue'
export default {
name: 'MainLayout',
components: {
AppHeader,
AppSidebar,
BreadcrumbNavigation
},
setup() {
//
const state = reactive({
screenWidth: window.innerWidth,
sidebarVisible: true,
isMobile: false,
isTablet: false,
isDesktop: false
})
//
defineOptions({
name: 'MainLayout'
})
const props = defineProps({
loading: {
type: Boolean,
default: false
}
})
//
const state = reactive({
screenWidth: window.innerWidth,
sidebarVisible: true,
isMobile: false,
isTablet: false,
isDesktop: false
})
//
const breakpoints = {
mobile: 768,
tablet: 1024
}
//
const breakpoints = {
mobile: 768,
tablet: 1024
}
const layoutClasses = computed(() => ({
'mobile-layout': state.isMobile,
'tablet-layout': state.isTablet,
'desktop-layout': state.isDesktop
}))
const layoutClasses = computed(() => ({
'mobile-layout': state.isMobile,
'tablet-layout': state.isTablet,
'desktop-layout': state.isDesktop
}))
//
const updateBreakpoints = () => {
state.screenWidth = window.innerWidth
state.isMobile = state.screenWidth < breakpoints.mobile
state.isTablet = state.screenWidth >= breakpoints.mobile && state.screenWidth < breakpoints.tablet
state.isDesktop = state.screenWidth >= breakpoints.tablet
//
if (state.isMobile) {
state.sidebarVisible = false
} else if (state.isDesktop) {
state.sidebarVisible = true
}
}
// 使
const sidebarVisible = computed(() => state.sidebarVisible)
const isMobile = computed(() => state.isMobile)
const isTablet = computed(() => state.isTablet)
const isDesktop = computed(() => state.isDesktop)
//
const toggleSidebar = () => {
if (state.isMobile) {
state.sidebarVisible = !state.sidebarVisible
} else {
state.sidebarVisible = !state.sidebarVisible
}
}
//
const handleNavigate = (route) => {
//
if (state.isMobile) {
state.sidebarVisible = false
}
}
//
let resizeTimer = null
const handleResize = () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
updateBreakpoints()
}, 150)
}
//
onMounted(() => {
updateBreakpoints()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
if (resizeTimer) {
clearTimeout(resizeTimer)
}
})
//
watch(() => window.location.pathname, () => {
if (state.isMobile) {
state.sidebarVisible = false
}
})
return {
sidebarVisible: computed(() => state.sidebarVisible),
isMobile: computed(() => state.isMobile),
isTablet: computed(() => state.isTablet),
isDesktop: computed(() => state.isDesktop),
layoutClasses,
toggleSidebar,
handleNavigate
}
//
const updateBreakpoints = () => {
state.screenWidth = window.innerWidth
state.isMobile = state.screenWidth < breakpoints.mobile
state.isTablet = state.screenWidth >= breakpoints.mobile && state.screenWidth < breakpoints.tablet
state.isDesktop = state.screenWidth >= breakpoints.tablet
//
if (state.isMobile) {
state.sidebarVisible = false
} else if (state.isDesktop) {
state.sidebarVisible = true
}
}
//
const toggleSidebar = () => {
if (state.isMobile) {
state.sidebarVisible = !state.sidebarVisible
} else {
state.sidebarVisible = !state.sidebarVisible
}
}
//
const handleNavigate = (route) => {
//
if (state.isMobile) {
state.sidebarVisible = false
}
}
//
let resizeTimer = null
const handleResize = () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
updateBreakpoints()
}, 150)
}
//
onMounted(() => {
updateBreakpoints()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
if (resizeTimer) {
clearTimeout(resizeTimer)
}
})
//
watch(() => window.location.pathname, () => {
if (state.isMobile) {
state.sidebarVisible = false
}
})
</script>
<style scoped>
@ -202,17 +192,7 @@ export default {
/* 移动端遮罩层 */
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
backdrop-filter: blur(2px);
}
/* 主内容区域 */
.main-content {
@ -222,6 +202,7 @@ export default {
width: 100%;
/* 移除 overflow: hidden 以允许页面滚动 */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.breadcrumb-container {

View File

@ -108,7 +108,7 @@ import { computed, ref, onMounted } from 'vue';
import ThreeModelViewer from '../ThreeModelViewer/index.vue';
import { MeshyServer } from '@deotaland/utils';
//
const emit = defineEmits(['clickModel','refineModel','save-project']);
const emit = defineEmits(['clickModel','refineModel','save-project','delete']);
const Meshy = new MeshyServer();
//
const showRightControls = ref(false);
@ -137,28 +137,30 @@ const handleGenerateModel = async () => {
let result;
if(props.cardData.taskId){
result = props.cardData.taskId;
TaskStatus(result);
TaskStatus(result,props.cardData.resultTask);
return
}
Meshy.createModelTask({
project_id: props.cardData.project_id,
image_url: props.cardData.imageUrl,
},(result)=>{
},(result,resultTask)=>{
if(result){
emit('save-project',{
...props.cardData,
resultTask:resultTask,
taskId:result
});
TaskStatus(result);
TaskStatus(result,resultTask);
}
},(error)=>{
console.error('模型生成失败:', error);
isGenerating.value = false;
progressPercentage.value = 0;
emit('delete');
},{})
};
const TaskStatus = (result)=>{
Meshy.getModelTaskStatus(result,(modelUrl)=>{
const TaskStatus = (result,resultTask)=>{
Meshy.getModelTaskStatus({
result:result,
resultTask:resultTask,
},(modelUrl)=>{
if(modelUrl){
//
generatedModelUrl.value = modelUrl;
@ -173,8 +175,7 @@ const TaskStatus = (result)=>{
}
},(error)=>{
console.error('模型生成失败:', error);
isGenerating.value = false;
progressPercentage.value = 0;
emit('delete');
},(progress)=>{
if (progress !== undefined) {
progressPercentage.value = progress;

View File

@ -18,20 +18,21 @@ import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import VueLazyload from 'vue3-lazyload'
import 'element-plus/dist/index.css'
import 'nprogress/nprogress.css'
import {ElMessage,ElLoading } from 'element-plus'
const app = createApp(App)
window.setElMessage = (options={})=>{
ElMessage[options.type || 'info'](options.message || '请求失败')
}
window.setElLoading = ()=>{
const loading = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.4)',
})
return loading
// loading.close()
}
// window.setElLoading = ()=>{
// const loading = ElLoading.service({
// lock: true,
// text: 'Loading',
// background: 'rgba(0, 0, 0, 0.4)',
// })
// return loading
// // loading.close()
// }
// Pinia
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

View File

@ -1,6 +1,6 @@
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import NProgress from 'nprogress'
const ModernHome = () => import('../views/ModernHome.vue')
const List = () => import('../views/List.vue')
const Login = () => import('../views/Login/Login.vue')
@ -15,7 +15,9 @@ const AgentManagement = () => import('../views/AgentManagement.vue')
const AddAgent = () => import('../views/AddAgent.vue')
const UiTest = () => import('../views/UiTest.vue')
const home = () => import('../views/home/index.vue')
NProgress.configure({
showSpinner: false,
})// 开启轻量模式(顶部细线)
// 路由配置
const routes = [
{
@ -128,9 +130,9 @@ const router = createRouter({
// 路由守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
// window.localStorage.setItem('token','123')
// return next()
// 检查是否需要登录
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
// 如果没有 token跳转到登录页
@ -139,16 +141,10 @@ router.beforeEach(async (to, from, next) => {
return
}
}
// 检查是否需要游客身份
if (to.meta.requiresGuest) {
const authStore = useAuthStore()
// 如果有 token跳转到首页
if (authStore.token) {
next('/')
return
}
}
next()
})
router.afterEach(() => {
NProgress.done()
})
export default router

View File

@ -45,7 +45,6 @@ export const useAuthStore = defineStore('auth', () => {
callback&&callback();
const router = useRouter();
localStorage.removeItem('token')
router.replace({ name: 'login' });
}
}

View File

@ -81,19 +81,6 @@
</div>
</div>
</el-scrollbar>
<!-- 初始加载蒙层 -->
<div v-if="loading && page === 1" class="overlay-loading">
<div class="loading-spinner"></div>
</div>
<!-- 加载更多状态 -->
<div v-if="loading" class="loading-more loading">
{{ $t('loading') }}
</div>
<div v-else-if="finished && projects.length > 0" class="loading-more">
{{ $t('allLoaded') }}
</div>
<!-- 垃圾箱抽屉 -->
<div
v-if="showTrash"
@ -111,65 +98,6 @@
<p>{{ $t('creationWorkspace.dropToDeleteHint') }}</p>
</div>
</div>
<!-- 全屏模态蒙层 -->
<div
v-if="showModal"
class="modal-overlay"
@click="closeModal"
>
<div class="modal-content" @click.stop>
<!-- 关闭按钮 -->
<button class="modal-close" @click="closeModal">
<CloseIcon class="close-icon" />
</button>
<!-- 模态内容 -->
<div class="modal-header">
<h2 class="modal-title">{{ modalTitle }}</h2>
<p class="modal-subtitle">{{ modalSubtitle }}</p>
</div>
<div class="modal-body">
<!-- 查看项目内容 -->
<div v-if="modalType === 'view'" class="modal-view-content">
<div class="project-preview">
<div class="preview-icon">
<FolderIcon />
</div>
<div class="preview-info">
<h3>{{ currentProject?.title }}</h3>
<p>{{ currentProject?.description }}</p>
<div class="preview-meta">
<span class="preview-type">{{ currentProject?.type }}</span>
<span class="preview-date">{{ formatDate(currentProject?.updatedAt) }}</span>
</div>
</div>
</div>
<button class="modal-action-btn primary" @click="openProject(currentProject)">
<ViewIcon class="btn-icon" />
进入项目
</button>
</div>
<!-- 更换缩略图内容 -->
<div v-if="modalType === 'edit'" class="modal-edit-content">
<div class="upload-area">
<div class="upload-icon">
<UploadIcon />
</div>
<h3>更换项目缩略图</h3>
<p>选择一张图片作为项目缩略图</p>
<button class="modal-action-btn" @click="triggerFileSelect">
<EditIcon class="btn-icon" />
选择图片
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 删除确认对话框 -->
<div
v-if="showDeleteConfirm"
@ -226,7 +154,6 @@ export default {
setup() {
const { t } = useI18n()
const router = useRouter()
//
const showModal = ref(false)
const modalType = ref('') // 'view' or 'edit'

View File

@ -23,6 +23,7 @@
<div class="google-login-section">
<GoogleOAuthButton
@success="handleLoginSuccess"
:loading="plugin.loading"
/>
</div>
@ -37,6 +38,7 @@
<div class="email-login-section">
<LoginForm
@login="handleLogin"
:loading="plugin.loading"
/>
</div>
<!-- 角色信息展示 -->
@ -68,7 +70,6 @@
<el-icon class="error-icon"><WarningFilled /></el-icon>
<span>{{ authStore.error }}</span>
</div>
<!-- 忘记密码和注册链接 -->
<div class="auth-links">
<div class="auth-links-row">

View File

@ -3,12 +3,15 @@ import { useRouter } from 'vue-router'
import {ElMessage} from 'element-plus';
import { useAuthStore } from '@/stores/auth.js';
export default class Login {
loading = false;//登录loading
constructor() {
this.router = useRouter();
this.authStore = useAuthStore();
}
async login(data) {
this.loading = true;
this.authStore.login(data,()=>{
this.loading = false;
this.router.push({ name: 'czhome' })
// this.refreshGoogleRefreshToken()
});

View File

@ -1,6 +1,5 @@
<template>
<div class="creative-zone" :style="{ '--grid-size': `${gridSize}px` }">
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': showSidebarOverlay }"></div>
<!-- 顶部固定头部组件 -->
<div class="header-wrapper">
<HeaderComponent :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
@ -72,6 +71,7 @@
v-else-if="card.type === 'model'"
:cardData="card"
:clickDisabled="isElementDragging"
@delete="handleDeleteCard(index)"
:projectId="projectId"
@save-project="(item)=>{handleSaveProject(index,item,'model')}"
@clickModel="handleModelClick"
@ -134,20 +134,6 @@ const selectedModel = ref(null);
const showImportModal = ref(false);
const importUrl = ref('https://xiaozhi.me/console/agents');
const showGuideModal = ref(false);
//
const showSidebarOverlay = ref(false);
// /
const showSidebarTransition = () => {
showSidebarOverlay.value = true;
};
const hideSidebarTransition = () => {
showSidebarOverlay.value = false;
};
//
const cleanupFunctions = ref({});
const projectId = ref(null);
@ -173,6 +159,7 @@ const handleSaveProject = (index,item,type='image')=>{
case 'model':
cardItem.modelUrl = item.modelUrl;
cardItem.taskId = item.taskId;
cardItem.resultTask = item.resultTask;
break;
}
cardItem.status = item.status;
@ -190,7 +177,6 @@ const createProject = async ()=>{
}
//
const getProjectInfo = async (id)=>{
showSidebarTransition();
const data = await PluginProject.getProject(id);
console.log(data,'data');
projectInfo.value = {...data};
@ -199,7 +185,6 @@ const getProjectInfo = async (id)=>{
...card,
id: card.id || Date.now() + Math.random().toString(36).substr(2, 9)
}));
hideSidebarTransition();
}
//
const updateProjectInfo = async (newProjectInfo)=>{
@ -442,7 +427,7 @@ const handleCreatePromptCard = (index,params) => {
const handleGenerateRequested = async (params) => {
// IPCard
const {count,profile,inspirationImage,ipType,ipTypeImg} = params
for (let i = 0; i < count; i++) {
for (let i = 0; i < count; i++) {
// 使
const newCard = createSmartCard({
imageUrl: null, // IPCard
@ -455,6 +440,8 @@ const handleGenerateRequested = async (params) => {
});
//
cards.value.push(newCard);
// 200
await new Promise(resolve => setTimeout(resolve, 200));
}
};
@ -886,75 +873,6 @@ html.dark .sidebar-container {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 侧边栏过渡动画蒙层 */
.sidebar-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(107, 70, 193, 0.1);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
/* 蒙层激活状态 */
.sidebar-overlay-active {
opacity: 1;
visibility: visible;
}
/* 转圈加载动画 */
.sidebar-overlay::before {
content: '';
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
}
.sidebar-overlay::after {
content: '';
position: absolute;
width: 70px;
height: 70px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
animation: spin 1.5s linear infinite reverse;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 暗色主题下的蒙层效果 */
html.dark .sidebar-overlay {
background: rgba(31, 41, 55, 0.85);
}
html.dark .sidebar-overlay::before {
border: 4px solid rgba(255, 255, 255, 0.2);
border-top: 4px solid #A78BFA;
box-shadow: 0 0 20px rgba(167, 139, 250, 0.5);
}
html.dark .sidebar-overlay::after {
border: 2px solid rgba(167, 139, 250, 0.1);
border-bottom: 2px solid rgba(167, 139, 250, 0.4);
}
/* 测试动画按钮样式 */
.test-animation-btn {

7
package-lock.json generated
View File

@ -43,6 +43,7 @@
"element-plus": "^2.11.7",
"jose": "^6.1.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"three": "^0.180.0",
@ -4943,6 +4944,12 @@
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==",
"license": "MIT"
},
"node_modules/nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
"license": "MIT"
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",

View File

@ -1,6 +1,6 @@
const login = {
LOGIN:{url:'/api-base/user/login',method:'POST'},// 登录
LOGOUT:{url:'/api-base/user/logout',method:'POST',isLoading:true},// 登出
LOGOUT:{url:'/api-base/user/logout',method:'POST',isLoading:true,isqp:true},// 登出
REGISTER:{url:'/api-base/user/register',method:'POST'},// 注册
SEND_EMAIL_CODE:{url:'/api-base/user/send-email-code',method:'POST'},// 发送邮箱验证码
OAUTH_GOOGLE:{url:'/api-base/user/oauth/google',method:'POST'},// google弹窗授权

View File

@ -1,5 +1,5 @@
const order = {
getOrderList:{url:'/api-core/front/order/list',method:'POST'},// 获取订单列表
getOrderList:{url:'/api-core/front/order/list',method:'POST',isLoading:true},// 获取订单列表
getOrderDetail:{url:'/api-core/front/order/get',method:'GET'},// 获取订单详情
orderCancel:{url:'/api-core/front/order/cancel',method:'POST'},// 取消订单支付
receiveAddress:{url:'/api-core/front/order/receive',method:'POST'},// 确认收货

View File

@ -19,11 +19,11 @@ const login = {
* 项目详情接口
* 引用示例: PROJECT_GET
*/
PROJECT_GET:{url:'/api-core/front/project/get',method:'GET'},
PROJECT_GET:{url:'/api-core/front/project/get',method:'GET',isLoading:true},
/**
* 项目列表接口
* 引用示例: PROJECT_LIST
*/
PROJECT_LIST:{url:'/api-core/front/project/list',method:'POST'},
PROJECT_LIST:{url:'/api-core/front/project/list',method:'POST',isLoading:true},
}
export default login;

View File

@ -19,10 +19,13 @@ const getPorjectType = () => {
};
export class GiminiServer extends FileServer {
RULE = getPorjectType();
// 任务并发队列
static taskQueue = new Map();
//最高并发限制
static MAX_CONCURRENT_TASKS = 4;
constructor() {
super();
}
/**
* 从URL获取MIME类型
* @param {*} url 图片URL
@ -141,8 +144,17 @@ export class GiminiServer extends FileServer {
}
})
};
//线上生模型
//线上生图片模型
async generateImageFromMultipleImagesOnline(baseImages, prompt,config={}){
if(GiminiServer.taskQueue.size >= GiminiServer.MAX_CONCURRENT_TASKS){
window.setElMessage({
type:'warning',
message:'Concurrent limit reached'
})
return Promise.reject('Concurrent limit reached');
}
const taskId = new Date().getTime();
GiminiServer.taskQueue.set(taskId, taskId);
return new Promise(async (resolve, reject) => {
// 标准化输入:确保 baseImages 是数组
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
@ -163,24 +175,12 @@ export class GiminiServer extends FileServer {
const imageParts = await Promise.all(images.map(async image =>{
return await this.dataUrlToGenerativePart(image,'url');
} ));
// 构建请求的 parts 数组
// const params = {
// "aspect_ratio": "9:16",
// "model": "gemini-2.5-flash-image",
// "location": "global",
// "vertexai": true,
// ...config,
// inputs: [
// ...imageParts,
// { text: prompt }
// ]
// }
const params = {
"aspect_ratio": "9:16",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview
"location": "global",
"vertexai": true,
...config,
const params = {
"aspect_ratio": "9:16",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
"location": "global",
"vertexai": true,
...config,
inputs: [
...imageParts,
{ text: prompt }
@ -188,19 +188,6 @@ const params = {
}
const requestUrl = this.RULE=='admin'?adminApi.default.GENERATE_IMAGE_ADMIN:clientApi.default.GENERATE_IMAGE;
const response = await requestUtils.common(requestUrl, params);
// const response = {
// "code": 0,
// "message": "",
// "success": true,
// "data": {
// "urls": [
// {
// "url": "/upload/51831c919193447e86843388ae31fc48.png",
// "mime_type": "image/png"
// }
// ]
// }
// }
if(response.data.error){
reject(response.data.error.message);
return;
@ -217,6 +204,9 @@ const params = {
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
} finally {
// 任务完成后从队列中移除
GiminiServer.taskQueue.delete(taskId);
}
})
}

View File

@ -16,12 +16,27 @@ const getPorjectType = () => {
};
export class MeshyServer extends FileServer {
RULE = getPorjectType();
// 任务并发队列
static taskQueue = new Map();
//最高并发限制
static MAX_CONCURRENT_TASKS = 1;
static pollingEnabled = true;
constructor() {
super();
}
//提交模型任务返回id
async createModelTask(item={},callback,errorCallback,config={}) {
// 检查当前并发任务数
if(MeshyServer.taskQueue.size >= MeshyServer.MAX_CONCURRENT_TASKS){
window.setElMessage({
type:'warning',
message:'Concurrent limit reached'
})
errorCallback&&errorCallback('Concurrent limit reached');
return Promise.reject('Concurrent limit reached');
}
const taskId = new Date().getTime();
MeshyServer.taskQueue.set(taskId, taskId);
try {
let params = {
project_id: item.project_id,
@ -51,7 +66,7 @@ export class MeshyServer extends FileServer {
// }
// };
if(response.code==0){
callback&&callback(response?.data?.result);
callback&&callback(response?.data?.result,taskId);
}
} catch (error) {
console.error('创建模型任务失败:', error);
@ -59,29 +74,36 @@ export class MeshyServer extends FileServer {
throw error;
}
}
static demoNum = 0;
//查询任务状态
async getModelTaskStatus(result,callback,errorCallback,progressCallback){
async getModelTaskStatus(resultItem,callback,errorCallback,progressCallback){
const result = resultItem.result;
const resultTask = resultItem.resultTask;
if(!MeshyServer.taskQueue.has(resultTask)){//用户刷新了页面清除了任务队列重新添加进任务队列
MeshyServer.taskQueue.set(resultTask, resultTask);
}
try {
const requestUrl = {
url:this.RULE=='admin'?adminApi.default.FIND_TASK_IDADMIN.url.replace('TASKID', result):clientApi.default.FIND_TASK_ID.url.replace('TASKID', result),
method:this.RULE=='admin'?adminApi.default.FIND_TASK_IDADMIN.method:clientApi.default.FIND_TASK_ID.method,
};
let response = await requestUtils.common(requestUrl, {});
if(response.code==0){
let data = response?.data
console.log(data,'查询任务状态');
switch (data.status) {
case 1:
// let modelurl = data.model_url.replace("https://assets.meshy.ai", "https://api.deotaland.ai/model");
let modelurl = data.result.s3_glb_url;
resultTask&&MeshyServer.taskQueue.delete(resultTask);
callback&&callback(modelurl);
break;
case 2:
errorCallback&&errorCallback();
resultTask&&MeshyServer.taskQueue.delete(resultTask);
break;
case 3:
errorCallback&&errorCallback();
resultTask&&MeshyServer.taskQueue.delete(resultTask);
break;
default:
if(!MeshyServer.pollingEnabled){//如果禁用轮询,直接返回
@ -90,9 +112,18 @@ export class MeshyServer extends FileServer {
// 等待三秒
await new Promise(resolve => setTimeout(resolve, 3000));
progressCallback&&progressCallback(data.progress);
this.getModelTaskStatus(result,callback,errorCallback,progressCallback);
this.getModelTaskStatus({
result:result,
resultTask:resultTask,
},callback,errorCallback,progressCallback);
break;
}
}
}
catch (error) {
resultTask&&MeshyServer.taskQueue.delete(resultTask);
errorCallback&&errorCallback(error);
throw error;
}
}
}

View File

@ -1,5 +1,5 @@
import axios from 'axios';
var ElLoading = null
var closeMethods = null
// 获取环境变量中的基础URL
const getEnvBaseURL = () => {
// 浏览器环境
@ -50,8 +50,8 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
response => {
if(ElLoading){
ElLoading.close()
if(closeMethods){
closeMethods.close()
}
// 直接返回响应数据
const res = response.data;
@ -183,7 +183,7 @@ export const request = {
requestConfig.data = data;
}
if(config.isLoading){
ElLoading = window.setElLoading()
closeMethods = window.setElLoading(config.isqp)
}
return service(requestConfig);
},

View File

@ -77,7 +77,7 @@ importers:
version: 5.44.1
unplugin-auto-import:
specifier: ^20.2.0
version: 20.2.0(@vueuse/core@14.1.0)
version: 20.2.0
unplugin-vue-components:
specifier: ^30.0.0
version: 30.0.0(vue@3.5.24)
@ -132,6 +132,9 @@ importers:
normalize.css:
specifier: ^8.0.1
version: 8.0.1
nprogress:
specifier: ^0.2.0
version: 0.2.0
pinia:
specifier: ^3.0.4
version: 3.0.4(vue@3.5.24)
@ -1285,10 +1288,6 @@ packages:
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
dev: false
/@types/web-bluetooth@0.0.21:
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
dev: true
/@types/webxr@0.5.24:
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
dev: false
@ -1424,17 +1423,6 @@ packages:
vue-demi: 0.13.11(vue@3.5.24)
dev: false
/@vueuse/core@14.1.0(vue@3.5.24):
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
peerDependencies:
vue: ^3.5.0
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.1.0
'@vueuse/shared': 14.1.0(vue@3.5.24)
vue: 3.5.24
dev: true
/@vueuse/core@9.13.0(vue@3.5.24):
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
dependencies:
@ -1447,22 +1435,10 @@ packages:
- vue
dev: false
/@vueuse/metadata@14.1.0:
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
dev: true
/@vueuse/metadata@9.13.0:
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
dev: false
/@vueuse/shared@14.1.0(vue@3.5.24):
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
peerDependencies:
vue: ^3.5.0
dependencies:
vue: 3.5.24
dev: true
/@vueuse/shared@9.13.0(vue@3.5.24):
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
dependencies:
@ -2924,6 +2900,10 @@ packages:
resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==}
dev: false
/nprogress@0.2.0:
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
dev: false
/nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
@ -3525,27 +3505,6 @@ packages:
unplugin-utils: 0.3.1
dev: true
/unplugin-auto-import@20.2.0(@vueuse/core@14.1.0):
resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==}
engines: {node: '>=14'}
peerDependencies:
'@nuxt/kit': ^4.0.0
'@vueuse/core': '*'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
'@vueuse/core':
optional: true
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.24)
local-pkg: 1.1.2
magic-string: 0.30.21
picomatch: 4.0.3
unimport: 5.5.0
unplugin: 2.3.10
unplugin-utils: 0.3.1
dev: true
/unplugin-icons@22.5.0:
resolution: {integrity: sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ==}
peerDependencies: