This commit is contained in:
13121765685 2025-11-26 12:28:57 +08:00
parent f9022e31c8
commit 55c9fe61fa
59 changed files with 3147 additions and 2192 deletions

View File

@ -12,6 +12,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@types/three": "^0.180.0",
"element-plus": "^2.11.7",
"pinia": "^2.2.6",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 304 KiB

View File

@ -12,7 +12,6 @@
</el-button>
<h1 class="admin-title">{{ t('admin.title') }}</h1>
</div>
<div class="header-right">
<!-- 主题切换 -->
<el-tooltip :content="t('header.toggleTheme')" placement="bottom">
@ -41,14 +40,14 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<!-- <el-dropdown-item command="profile">
<el-icon><User /></el-icon>
{{ t('admin.layout.profile') }}
</el-dropdown-item>
<el-dropdown-item command="settings">
<el-icon><Setting /></el-icon>
{{ t('admin.layout.settings') }}
</el-dropdown-item>
</el-dropdown-item> -->
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
{{ t('admin.layout.logout') }}
@ -138,8 +137,6 @@ import {
Moon,
Switch,
ArrowDown,
User,
Setting,
SwitchButton,
DataAnalysis,
Document,
@ -147,20 +144,19 @@ import {
UserFilled,
EditPen
} from '@element-plus/icons-vue'
const { t, locale } = useI18n()
import { AdminLogin } from '../../views/AdminLogin/AdminLogin'
const { t, locale } = useI18n();
const Plug = new AdminLogin();
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const appStore = useAppStore()
const sidebarCollapsed = ref(false)
const mobileMenuVisible = ref(false)
//
const isDark = computed(() => appStore.isDarkTheme)
const currentLocale = computed(() => locale.value)
const username = computed(() => authStore.username)
const username = computed(() => authStore?.user?.nickname)
const activeMenu = computed(() => route.path)
// index -> indexes
@ -225,19 +221,11 @@ const handleUserAction = async (command) => {
break
case 'logout':
try {
await ElMessageBox.confirm(
t('messages.confirmLogout'),
t('messages.logout'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
)
authStore.logout()
ElMessage.success(t('messages.logoutSuccess'))
router.push('/admin/login')
Plug.logout(()=>{
localStorage.removeItem('token');
localStorage.removeItem('user');
router.replace('/login')
})
} catch {
//
}

View File

@ -123,7 +123,10 @@ export default {
passwordPlaceholder: 'Please enter password',
loginSuccess: 'Login successful',
loginFailed: 'Login failed, please check username and password',
welcome: 'Welcome to Admin Panel'
welcome: 'Welcome to Admin Panel',
captcha: 'Verification Code',
captchaRequired: 'Please enter verification code',
captchaLength: 'Verification code should be 4-6 characters'
},
common: {
refresh: 'Refresh',

View File

@ -107,7 +107,10 @@ export default {
passwordPlaceholder: '请输入密码',
loginSuccess: '登录成功',
loginFailed: '登录失败,请检查用户名和密码',
welcome: '欢迎使用管理后台'
welcome: '欢迎使用管理后台',
captcha: '验证码',
captchaRequired: '请输入验证码',
captchaLength: '验证码长度应为4-6位'
},
dashboard: {
title: '仪表板',

View File

@ -1,7 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import About from '@/views/About.vue'
import NotFound from '@/views/NotFound.vue'
import AdminLogin from '@/views/AdminLogin.vue'
import AdminLogin from '@/views/AdminLogin/AdminLogin.vue'
// 管理员布局组件(懒加载)
const AdminLayout = () => import('@/components/admin/AdminLayout.vue')
@ -17,7 +17,7 @@ const routes = [
{
path: '/',
name: 'Home',
redirect: '/admin/login',
redirect: '/login',
meta: {
title: '首页重定向'
}
@ -31,11 +31,11 @@ const routes = [
}
},
{
path: '/admin/login',
name: 'AdminLogin',
path: '/login',
name: 'Login',
component: AdminLogin,
meta: {
title: '管理员登录',
title: '登录',
requiresAuth: false
}
},
@ -140,22 +140,21 @@ router.beforeEach((to, from, next) => {
// 检查是否需要认证
if (to.meta?.requiresAuth) {
const token = localStorage.getItem('admin-token')
const token = localStorage.getItem('token')
if (!token) {
next('/admin/login')
next('/login')
return
}
}
// 如果已登录且访问登录页面,重定向到管理后台首页
if (to.path === '/admin/login') {
const token = localStorage.getItem('admin-token')
if (to.path === '/login') {
const token = localStorage.getItem('token')
if (token) {
next('/admin')
return
}
}
next()
})

View File

@ -1,265 +0,0 @@
/**
* API 服务模块
* 提供 HTTP 请求封装和 API 调用的统一接口
*/
import axios from 'axios'
import { useAppStore } from '../stores'
// 创建 axios 实例
const createApiClient = () => {
const client = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
client.interceptors.request.use(
(config) => {
// 添加认证 token
const token = localStorage.getItem('auth-token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加请求时间戳
config.metadata = { startTime: Date.now() }
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, config)
return config
},
(error) => {
console.error('[API Request Error]', error)
return Promise.reject(error)
}
)
// 响应拦截器
client.interceptors.response.use(
(response) => {
// 计算请求耗时
const endTime = Date.now()
const duration = endTime - response.config.metadata?.startTime
console.log(`[API Response] ${response.config.method?.toUpperCase()} ${response.config.url} (${duration}ms)`, response.data)
return response.data
},
(error) => {
const { response, config } = error
const status = response?.status
const message = response?.data?.message || error.message
console.error(`[API Error] ${config?.method?.toUpperCase()} ${config?.url} - ${status}`, message, response?.data)
// 统一错误处理
if (status === 401) {
// Token 过期,清除认证信息
localStorage.removeItem('auth-token')
localStorage.removeItem('user-info')
window.location.href = '/login'
} else if (status === 403) {
// 权限不足
console.warn('Access denied')
} else if (status >= 500) {
// 服务器错误
console.error('Server error')
}
return Promise.reject({
status,
message,
data: response?.data,
config: config
})
}
)
return client
}
// 创建 API 客户端实例
const api = createApiClient()
// 通用 API 方法
export const apiClient = {
// GET 请求
get: (url, config = {}) => api.get(url, config),
// POST 请求
post: (url, data = {}, config = {}) => api.post(url, data, config),
// PUT 请求
put: (url, data = {}, config = {}) => api.put(url, data, config),
// PATCH 请求
patch: (url, data = {}, config = {}) => api.patch(url, data, config),
// DELETE 请求
delete: (url, config = {}) => api.delete(url, config),
// 文件上传
upload: (url, formData, config = {}) => api.post(url, formData, {
...config,
headers: {
'Content-Type': 'multipart/form-data',
...config.headers
}
}),
// 下载文件
download: async (url, filename, config = {}) => {
const response = await api.get(url, {
...config,
responseType: 'blob'
})
// 创建下载链接
const blob = new Blob([response])
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename
document.body.appendChild(link)
link.click()
link.remove()
window.URL.revokeObjectURL(downloadUrl)
}
}
// API 错误处理类
export class ApiError extends Error {
constructor(status, message, data = null, config = null) {
super(message)
this.name = 'ApiError'
this.status = status
this.data = data
this.config = config
}
}
// 重试机制
export const withRetry = async (apiCall, maxRetries = 3, delay = 1000) => {
let lastError
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await apiCall()
} catch (error) {
lastError = error
// 最后一次尝试失败
if (attempt === maxRetries) {
break
}
// 如果是认证错误,不重试
if (error.status === 401) {
break
}
// 如果是客户端错误,不重试
if (error.status >= 400 && error.status < 500) {
break
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, delay * attempt))
}
}
throw lastError
}
// API 请求缓存
const cache = new Map()
export const withCache = (key, apiCall, ttl = 5 * 60 * 1000) => {
const cached = cache.get(key)
const now = Date.now()
if (cached && (now - cached.timestamp) < ttl) {
return Promise.resolve(cached.data)
}
return apiCall().then(data => {
cache.set(key, { data, timestamp: now })
return data
})
}
// 清除缓存
export const clearCache = (pattern = null) => {
if (pattern) {
const regex = new RegExp(pattern)
for (const key of cache.keys()) {
if (regex.test(key)) {
cache.delete(key)
}
}
} else {
cache.clear()
}
}
// 离线检测
export const isOnline = () => {
return navigator.onLine
}
// 离线队列处理
class OfflineQueue {
constructor() {
this.queue = []
this.isProcessing = false
// 监听网络状态
window.addEventListener('online', () => this.processQueue())
window.addEventListener('offline', () => console.log('App is offline'))
}
add(request) {
this.queue.push(request)
if (isOnline()) {
this.processQueue()
}
}
async processQueue() {
if (this.isProcessing || !isOnline()) {
return
}
this.isProcessing = true
while (this.queue.length > 0) {
const request = this.queue.shift()
try {
await apiClient[request.method](request.url, request.data, request.config)
console.log(`Offline request processed: ${request.method} ${request.url}`)
} catch (error) {
console.error(`Failed to process offline request: ${request.url}`, error)
// 如果失败,重新加入队列
this.queue.unshift(request)
break
}
}
this.isProcessing = false
}
}
export const offlineQueue = new OfflineQueue()
// 导出工具函数
export default {
apiClient,
ApiError,
withRetry,
withCache,
clearCache,
isOnline,
offlineQueue
}

View File

@ -3,8 +3,8 @@ import { defineStore } from 'pinia'
// 管理员认证状态管理store
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('admin-token') || '',
user: JSON.parse(localStorage.getItem('admin-user') || 'null')
token: localStorage.getItem('token') || '',
user: JSON.parse(localStorage.getItem('user') || 'null')
}),
getters: {
@ -18,15 +18,14 @@ export const useAuthStore = defineStore('auth', {
actions: {
// 登录
login(loginData) {
const { token, user } = loginData
this.token = token
this.user = user
login(loginData,callback) {
const { accessToken} = loginData
this.token = accessToken
this.user = loginData
// 持久化存储
localStorage.setItem('admin-token', token)
localStorage.setItem('admin-user', JSON.stringify(user))
localStorage.setItem('token', accessToken)
localStorage.setItem('user', JSON.stringify(this.user))
callback&&callback();
},
// 登出
@ -35,15 +34,15 @@ export const useAuthStore = defineStore('auth', {
this.user = null
// 清除持久化存储
localStorage.removeItem('admin-token')
localStorage.removeItem('admin-user')
localStorage.removeItem('token')
localStorage.removeItem('user')
localStorage.removeItem('admin-remember-me')
},
// 检查认证状态
checkAuth() {
const storedToken = localStorage.getItem('admin-token')
const storedUser = localStorage.getItem('admin-user')
const storedToken = localStorage.getItem('token')
const storedUser = localStorage.getItem('user')
if (storedToken && storedUser) {
this.token = storedToken

View File

@ -0,0 +1,367 @@
/**
* 统一错误处理工具
* 提供错误分类重试机制错误恢复策略等功能
*/
import { createLogger } from './logger.js';
const logger = createLogger('ErrorHandler');
// 错误类型定义
export const ERROR_TYPES = {
NETWORK: 'NETWORK_ERROR',
TIMEOUT: 'TIMEOUT_ERROR',
VALIDATION: 'VALIDATION_ERROR',
AUTHENTICATION: 'AUTH_ERROR',
AUTHORIZATION: 'AUTHORIZATION_ERROR',
SERVER: 'SERVER_ERROR',
CLIENT: 'CLIENT_ERROR',
UNKNOWN: 'UNKNOWN_ERROR',
AI_SERVICE: 'AI_SERVICE_ERROR',
RATE_LIMIT: 'RATE_LIMIT_ERROR'
};
// 错误严重程度
export const ERROR_SEVERITY = {
LOW: 'low',
MEDIUM: 'medium',
HIGH: 'high',
CRITICAL: 'critical'
};
/**
* 自定义错误类
*/
export class AppError extends Error {
constructor(message, type = ERROR_TYPES.UNKNOWN, severity = ERROR_SEVERITY.MEDIUM, details = {}) {
super(message);
this.name = 'AppError';
this.type = type;
this.severity = severity;
this.details = details;
this.timestamp = new Date().toISOString();
this.retryable = this.isRetryable();
}
/**
* 判断错误是否可重试
*/
isRetryable() {
const retryableTypes = [
ERROR_TYPES.NETWORK,
ERROR_TYPES.TIMEOUT,
ERROR_TYPES.SERVER,
ERROR_TYPES.RATE_LIMIT
];
return retryableTypes.includes(this.type);
}
/**
* 获取用户友好的错误消息
*/
getUserMessage() {
const userMessages = {
[ERROR_TYPES.NETWORK]: '网络连接异常,请检查网络设置后重试',
[ERROR_TYPES.TIMEOUT]: '请求超时,请稍后重试',
[ERROR_TYPES.VALIDATION]: '输入信息有误,请检查后重新提交',
[ERROR_TYPES.AUTHENTICATION]: '身份验证失败,请重新登录',
[ERROR_TYPES.AUTHORIZATION]: '权限不足,无法执行此操作',
[ERROR_TYPES.SERVER]: '服务器暂时无法响应,请稍后重试',
[ERROR_TYPES.CLIENT]: '客户端错误,请刷新页面后重试',
[ERROR_TYPES.AI_SERVICE]: 'AI服务暂时不可用请稍后重试',
[ERROR_TYPES.RATE_LIMIT]: '请求过于频繁,请稍后重试',
[ERROR_TYPES.UNKNOWN]: '发生未知错误,请联系技术支持'
};
return userMessages[this.type] || this.message;
}
}
/**
* 错误分类器 - 根据错误信息自动分类
* @param {Error} error - 原始错误对象
* @returns {AppError} 分类后的错误对象
*/
export const classifyError = (error) => {
if (error instanceof AppError) {
return error;
}
let type = ERROR_TYPES.UNKNOWN;
let severity = ERROR_SEVERITY.MEDIUM;
const details = {
originalError: error.message,
stack: error.stack
};
// 网络相关错误
if (error.code === 'NETWORK_ERROR' ||
error.message.includes('Network Error') ||
error.message.includes('fetch')) {
type = ERROR_TYPES.NETWORK;
severity = ERROR_SEVERITY.HIGH;
}
// 超时错误
else if (error.code === 'ECONNABORTED' ||
error.message.includes('timeout') ||
error.message.includes('Timeout')) {
type = ERROR_TYPES.TIMEOUT;
severity = ERROR_SEVERITY.MEDIUM;
}
// HTTP状态码错误
else if (error.response) {
const status = error.response.status;
details.httpStatus = status;
details.responseData = error.response.data;
if (status === 401) {
type = ERROR_TYPES.AUTHENTICATION;
severity = ERROR_SEVERITY.HIGH;
} else if (status === 403) {
type = ERROR_TYPES.AUTHORIZATION;
severity = ERROR_SEVERITY.HIGH;
} else if (status === 429) {
type = ERROR_TYPES.RATE_LIMIT;
severity = ERROR_SEVERITY.MEDIUM;
} else if (status >= 400 && status < 500) {
type = ERROR_TYPES.CLIENT;
severity = ERROR_SEVERITY.MEDIUM;
} else if (status >= 500) {
type = ERROR_TYPES.SERVER;
severity = ERROR_SEVERITY.HIGH;
}
}
// 验证错误
else if (error.message.includes('validation') ||
error.message.includes('invalid') ||
error.message.includes('required')) {
type = ERROR_TYPES.VALIDATION;
severity = ERROR_SEVERITY.LOW;
}
return new AppError(error.message, type, severity, details);
};
/**
* 重试机制配置
*/
const RETRY_CONFIG = {
maxAttempts: 3,
baseDelay: 1000, // 基础延迟时间(毫秒)
maxDelay: 10000, // 最大延迟时间(毫秒)
backoffFactor: 2 // 退避因子
};
/**
* 计算重试延迟时间指数退避
* @param {number} attempt - 当前重试次数
* @returns {number} 延迟时间毫秒
*/
const calculateRetryDelay = (attempt) => {
const delay = RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffFactor, attempt - 1);
return Math.min(delay, RETRY_CONFIG.maxDelay);
};
/**
* 带重试机制的函数执行器
* @param {Function} fn - 要执行的异步函数
* @param {Object} options - 重试选项
* @returns {Promise} 执行结果
*/
export const withRetry = async (fn, options = {}) => {
const config = { ...RETRY_CONFIG, ...options };
let lastError;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
logger.debug(`Executing function, attempt ${attempt}/${config.maxAttempts}`);
const result = await fn();
if (attempt > 1) {
logger.info(`Function succeeded on attempt ${attempt}`);
}
return result;
} catch (error) {
const classifiedError = classifyError(error);
lastError = classifiedError;
logger.warn(`Attempt ${attempt} failed`, {
error: classifiedError.message,
type: classifiedError.type,
retryable: classifiedError.retryable
});
// 如果错误不可重试,直接抛出
if (!classifiedError.retryable) {
throw classifiedError;
}
// 如果是最后一次尝试,抛出错误
if (attempt === config.maxAttempts) {
logger.error(`All ${config.maxAttempts} attempts failed`, {
finalError: classifiedError.message,
type: classifiedError.type
});
throw classifiedError;
}
// 等待后重试
const delay = calculateRetryDelay(attempt);
logger.debug(`Waiting ${delay}ms before retry`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
/**
* 错误恢复策略
*/
export const ErrorRecoveryStrategies = {
/**
* 网络错误恢复
*/
async networkError(error, context = {}) {
logger.info('Attempting network error recovery');
// 检查网络连接
if (typeof navigator !== 'undefined' && !navigator.onLine) {
throw new AppError(
'网络连接已断开,请检查网络设置',
ERROR_TYPES.NETWORK,
ERROR_SEVERITY.HIGH
);
}
// 尝试使用备用端点
if (context.fallbackUrl) {
logger.info('Trying fallback URL', { fallbackUrl: context.fallbackUrl });
// 这里可以实现备用端点逻辑
}
throw error;
},
/**
* 认证错误恢复
*/
async authError(error, context = {}) {
logger.info('Attempting auth error recovery');
// 尝试刷新token
if (context.refreshToken) {
try {
logger.debug('Attempting token refresh');
// 这里可以实现token刷新逻辑
// const newToken = await refreshAuthToken(context.refreshToken);
// return newToken;
} catch (refreshError) {
logger.error('Token refresh failed', refreshError);
}
}
// 清除本地认证信息
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
}
throw error;
},
/**
* 服务器错误恢复
*/
async serverError(error, context = {}) {
logger.info('Attempting server error recovery');
// 检查服务状态
if (context.healthCheckUrl) {
try {
// 这里可以实现健康检查逻辑
logger.debug('Checking service health');
} catch (healthError) {
logger.warn('Health check failed', healthError);
}
}
throw error;
}
};
/**
* 全局错误处理器
* @param {Error} error - 错误对象
* @param {Object} context - 错误上下文
* @returns {AppError} 处理后的错误
*/
export const handleError = async (error, context = {}) => {
const classifiedError = classifyError(error);
logger.error('Handling error', {
type: classifiedError.type,
severity: classifiedError.severity,
message: classifiedError.message,
context
});
// 尝试错误恢复
try {
switch (classifiedError.type) {
case ERROR_TYPES.NETWORK:
return await ErrorRecoveryStrategies.networkError(classifiedError, context);
case ERROR_TYPES.AUTHENTICATION:
return await ErrorRecoveryStrategies.authError(classifiedError, context);
case ERROR_TYPES.SERVER:
return await ErrorRecoveryStrategies.serverError(classifiedError, context);
default:
// 无特殊恢复策略,直接返回错误
break;
}
} catch (recoveryError) {
logger.error('Error recovery failed', recoveryError);
}
return classifiedError;
};
/**
* 创建错误边界包装器
* @param {Function} fn - 要包装的函数
* @param {Object} options - 选项
* @returns {Function} 包装后的函数
*/
export const createErrorBoundary = (fn, options = {}) => {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
const handledError = await handleError(error, options.context);
if (options.fallback) {
logger.info('Using fallback function');
return await options.fallback(handledError, ...args);
}
throw handledError;
}
};
};
export default {
AppError,
classifyError,
withRetry,
handleError,
createErrorBoundary,
ERROR_TYPES,
ERROR_SEVERITY
};

View File

@ -0,0 +1,141 @@
/**
* 图片处理工具函数
*/
/**
* 将本地路径的图片转换为DataURL格式
* @param {string} imagePath - 图片路径可以是本地路径或URL
* @returns {Promise<string>} - 返回DataURL格式的图片数据
*/
export const convertImageToDataURL = async (imagePath) => {
return new Promise((resolve, reject) => {
// 如果已经是DataURL格式直接返回
if (imagePath.startsWith('data:image/')) {
resolve(imagePath);
return;
}
// 创建Image对象
const img = new Image();
// 设置跨域属性(如果是网络图片)
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
// 创建canvas元素
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置canvas尺寸
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
// 绘制图片到canvas
ctx.drawImage(img, 0, 0);
// 转换为DataURL
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL);
} catch (error) {
reject(new Error(`转换图片失败: ${error.message}`));
}
};
img.onerror = () => {
reject(new Error(`无法加载图片: ${imagePath}`));
};
// 设置图片源
img.src = imagePath;
});
};
/**
* 批量转换图片为DataURL格式
* @param {string[]} imagePaths - 图片路径数组
* @returns {Promise<string[]>} - 返回DataURL格式的图片数据数组
*/
export const convertImagesToDataURL = async (imagePaths) => {
if (!Array.isArray(imagePaths)) {
throw new Error('imagePaths必须是数组');
}
const promises = imagePaths.map(path => convertImageToDataURL(path));
return Promise.all(promises);
};
/**
* 验证图片路径是否有效
* @param {string} imagePath - 图片路径
* @returns {boolean} - 是否为有效的图片路径
*/
export const isValidImagePath = (imagePath) => {
if (!imagePath || typeof imagePath !== 'string') {
return false;
}
// 检查是否是DataURL格式
if (imagePath.startsWith('data:image/')) {
return true;
}
// 检查是否是有效的图片文件扩展名
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
const lowerPath = imagePath.toLowerCase();
return imageExtensions.some(ext => lowerPath.includes(ext));
};
/**
* 获取图片信息尺寸格式等
* @param {string} imagePath - 图片路径
* @returns {Promise<Object>} - 图片信息对象
*/
export const getImageInfo = async (imagePath) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
resolve({
width: img.naturalWidth,
height: img.naturalHeight,
aspectRatio: img.naturalWidth / img.naturalHeight
});
};
img.onerror = () => {
reject(new Error(`无法加载图片: ${imagePath}`));
};
img.src = imagePath;
});
};
/**
* 将base64图片数组转换为FormData格式
* @param {Array} imageArray - 包含base64图片的数组格式为 [{type: "png", url: "data:image/png;base64,..."}]
* @returns {FormData} - FormData对象包含所有图片文件
*/
export const convertImageArrayToFormData = (imageArray) => {
const formData = new FormData();
imageArray.forEach((imageObj, index) => {
// 从base64 DataURL中提取实际的base64数据
const base64Data = imageObj.url.split(',')[1];
// 将base64转换为Blob
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: `image/${imageObj.type}` });
// 添加到FormData使用索引作为字段名
formData.append(`image_${index}`, blob, `view_${index}.${imageObj.type}`);
});
return formData;
};

View File

@ -0,0 +1,247 @@
/**
* 统一日志记录工具
* 提供结构化的日志记录功能支持不同级别的日志输出
*/
// 日志级别定义
const LOG_LEVELS = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
};
// 当前日志级别(可通过环境变量配置)
const CURRENT_LOG_LEVEL = process.env.NODE_ENV === 'production'
? LOG_LEVELS.WARN
: LOG_LEVELS.DEBUG;
// 日志颜色配置(仅在开发环境使用)
const LOG_COLORS = {
ERROR: '\x1b[31m', // 红色
WARN: '\x1b[33m', // 黄色
INFO: '\x1b[36m', // 青色
DEBUG: '\x1b[37m', // 白色
RESET: '\x1b[0m' // 重置
};
/**
* 格式化日志消息
* @param {string} level - 日志级别
* @param {string} module - 模块名称
* @param {string} message - 日志消息
* @param {any} data - 附加数据
* @returns {string} 格式化后的日志消息
*/
const formatLogMessage = (level, module, message, data) => {
const timestamp = new Date().toISOString();
const color = LOG_COLORS[level] || '';
const reset = LOG_COLORS.RESET;
let formattedMessage = `${color}[${timestamp}] [${level}] [${module}] ${message}${reset}`;
if (data !== null && data !== undefined) {
if (typeof data === 'object') {
formattedMessage += `\n${JSON.stringify(data, null, 2)}`;
} else {
formattedMessage += ` ${data}`;
}
}
return formattedMessage;
};
/**
* 通用日志记录函数
* @param {string} level - 日志级别
* @param {string} module - 模块名称
* @param {string} message - 日志消息
* @param {any} data - 附加数据
*/
const log = (level, module, message, data = null) => {
const levelValue = LOG_LEVELS[level];
// 检查是否应该输出此级别的日志
if (levelValue > CURRENT_LOG_LEVEL) {
return;
}
const formattedMessage = formatLogMessage(level, module, message, data);
// 根据级别选择输出方法
switch (level) {
case 'ERROR':
console.error(formattedMessage);
break;
case 'WARN':
console.warn(formattedMessage);
break;
case 'INFO':
console.info(formattedMessage);
break;
case 'DEBUG':
console.log(formattedMessage);
break;
default:
console.log(formattedMessage);
}
// 在生产环境中,可以在这里添加日志上报逻辑
if (process.env.NODE_ENV === 'production' && level === 'ERROR') {
// TODO: 添加错误日志上报到监控系统
// reportErrorToMonitoring(module, message, data);
}
};
/**
* 创建模块专用的日志记录器
* @param {string} moduleName - 模块名称
* @returns {Object} 日志记录器对象
*/
export const createLogger = (moduleName) => {
return {
error: (message, data = null) => log('ERROR', moduleName, message, data),
warn: (message, data = null) => log('WARN', moduleName, message, data),
info: (message, data = null) => log('INFO', moduleName, message, data),
debug: (message, data = null) => log('DEBUG', moduleName, message, data),
// 性能监控相关
time: (label) => {
if (CURRENT_LOG_LEVEL >= LOG_LEVELS.DEBUG) {
console.time(`[${moduleName}] ${label}`);
}
},
timeEnd: (label) => {
if (CURRENT_LOG_LEVEL >= LOG_LEVELS.DEBUG) {
console.timeEnd(`[${moduleName}] ${label}`);
}
},
// 分组日志
group: (label) => {
if (CURRENT_LOG_LEVEL >= LOG_LEVELS.DEBUG) {
console.group(`[${moduleName}] ${label}`);
}
},
groupEnd: () => {
if (CURRENT_LOG_LEVEL >= LOG_LEVELS.DEBUG) {
console.groupEnd();
}
}
};
};
/**
* 默认日志记录器
*/
export const logger = createLogger('App');
/**
* 错误边界日志记录
* @param {Error} error - 错误对象
* @param {string} context - 错误上下文
* @param {Object} additionalInfo - 附加信息
*/
export const logError = (error, context = 'Unknown', additionalInfo = {}) => {
const errorInfo = {
message: error.message,
stack: error.stack,
name: error.name,
context,
timestamp: new Date().toISOString(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'Server',
url: typeof window !== 'undefined' ? window.location.href : 'N/A',
...additionalInfo
};
logger.error('Unhandled error occurred', errorInfo);
// 在生产环境中上报错误
if (process.env.NODE_ENV === 'production') {
// TODO: 实现错误上报逻辑
// reportErrorToService(errorInfo);
}
};
/**
* API请求日志记录
* @param {string} method - HTTP方法
* @param {string} url - 请求URL
* @param {number} status - 响应状态码
* @param {number} duration - 请求耗时毫秒
* @param {Object} additionalInfo - 附加信息
*/
export const logApiRequest = (method, url, status, duration, additionalInfo = {}) => {
const logData = {
method,
url,
status,
duration: `${duration}ms`,
timestamp: new Date().toISOString(),
...additionalInfo
};
if (status >= 400) {
logger.error(`API request failed: ${method} ${url}`, logData);
} else if (duration > 3000) {
logger.warn(`Slow API request: ${method} ${url}`, logData);
} else {
logger.info(`API request: ${method} ${url}`, logData);
}
};
/**
* 性能监控日志
* @param {string} operation - 操作名称
* @param {number} duration - 操作耗时毫秒
* @param {Object} metadata - 元数据
*/
export const logPerformance = (operation, duration, metadata = {}) => {
const perfData = {
operation,
duration: `${duration}ms`,
timestamp: new Date().toISOString(),
...metadata
};
if (duration > 5000) {
logger.warn(`Slow operation detected: ${operation}`, perfData);
} else {
logger.debug(`Performance: ${operation}`, perfData);
}
};
/**
* 用户行为日志记录
* @param {string} action - 用户操作
* @param {Object} details - 操作详情
*/
export const logUserAction = (action, details = {}) => {
const actionData = {
action,
timestamp: new Date().toISOString(),
sessionId: getSessionId(),
...details
};
logger.info(`User action: ${action}`, actionData);
};
/**
* 获取会话ID简单实现
*/
const getSessionId = () => {
if (typeof window !== 'undefined') {
let sessionId = sessionStorage.getItem('sessionId');
if (!sessionId) {
sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2);
sessionStorage.setItem('sessionId', sessionId);
}
return sessionId;
}
return 'server-session';
};
export default logger;

View File

@ -0,0 +1,91 @@
// 被动事件监听器优化工具
// 用于解决滚动性能警告问题
/**
* 创建被动事件监听器
* @param {HTMLElement} element - 要添加监听器的DOM元素
* @param {string} event - 事件名称
* @param {Function} handler - 事件处理函数
* @param {Object} options - 其他选项
*/
export function addPassiveEventListener(element, event, handler, options = {}) {
const passiveOptions = {
passive: true,
...options
};
element.addEventListener(event, handler, passiveOptions);
return () => {
element.removeEventListener(event, handler, passiveOptions);
};
}
/**
* 为滚动相关事件创建被动监听器
* @param {HTMLElement} element - DOM元素
* @param {Object} handlers - 事件处理器对象 { touchstart: fn, touchmove: fn, wheel: fn }
* @returns {Object} 清理函数集合
*/
export function addScrollPassiveListeners(element, handlers = {}) {
const cleanupFunctions = {};
// 为每个滚动相关事件创建被动监听器
Object.entries(handlers).forEach(([event, handler]) => {
if (typeof handler === 'function') {
cleanupFunctions[event] = addPassiveEventListener(element, event, handler);
}
});
return {
cleanup: () => {
Object.values(cleanupFunctions).forEach(cleanup => {
if (typeof cleanup === 'function') {
cleanup();
}
});
}
};
}
/**
* 为文档滚动创建被动监听器
* @param {Object} handlers - 事件处理器
* @returns {Object} 清理函数
*/
export function addDocumentScrollListeners(handlers = {}) {
return addScrollPassiveListeners(document, handlers);
}
/**
* 为窗口滚动创建被动监听器
* @param {Object} handlers - 事件处理器
* @returns {Object} 清理函数
*/
export function addWindowScrollListeners(handlers = {}) {
return addScrollPassiveListeners(window, handlers);
}
// 导出预定义的事件类型
export const SCROLL_EVENTS = {
touchstart: 'touchstart',
touchmove: 'touchmove',
wheel: 'wheel',
scroll: 'scroll'
};
// 导出预定义的滚动事件处理器示例
export const DEFAULT_SCROLL_HANDLERS = {
touchstart: (e) => {
// 触摸开始事件处理
// 可以在这里添加自定义逻辑
},
touchmove: (e) => {
// 触摸移动事件处理
// 可以在这里添加自定义逻辑
},
wheel: (e) => {
// 鼠标滚轮事件处理
// 可以在这里添加自定义逻辑
}
};

View File

@ -0,0 +1,29 @@
import { requestUtils,clientApi } from '@deotaland/utils'
export class AdminLogin {
constructor() {
}
//获取验证码
async getCaptchaCode(callback) {
const res = await requestUtils.common(clientApi.default.CAPTCHA_CODE);
if (res.code === 200) {
// 确保base64数据包含正确的data URL前缀
let imgData = res.data.img;
if (imgData && !imgData.startsWith('data:image')) {
imgData = 'data:image/png;base64,' + imgData;
}
callback({
uuid:res.data.uuid,
img:imgData,
});
}
}
//退出登录
logout(callback) {
requestUtils.common(clientApi.default.LOGOUT).then(res=>{
if(res.code === 200){
callback&&callback();
}
})
}
}

View File

@ -40,6 +40,34 @@
</el-input>
</el-form-item>
<!-- 验证码输入 -->
<el-form-item prop="code">
<div class="captcha-container">
<el-input
v-model="loginForm.code"
:placeholder="t('admin.login.captcha')"
clearable
@keyup.enter="handleLogin"
class="captcha-input"
>
<template #prefix>
<el-icon><Key /></el-icon>
</template>
</el-input>
<div class="captcha-image" @click="getCaptchaCode">
<img
v-if="codeImg"
:src="codeImg"
alt="验证码"
class="captcha-img"
/>
<div v-else class="captcha-loading">
<el-icon class="is-loading"><Loading /></el-icon>
</div>
</div>
</div>
</el-form-item>
<el-form-item>
<div class="login-options">
<el-checkbox v-model="loginForm.rememberMe">
@ -69,8 +97,10 @@ import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores'
import { UserFilled, Lock } from '@element-plus/icons-vue'
import { UserFilled, Lock, Key, Loading } from '@element-plus/icons-vue'
import { requestUtils,clientApi } from '@deotaland/utils'
import {AdminLogin} from './AdminLogin'
const PLUG = new AdminLogin();
const { t } = useI18n()
const router = useRouter()
const authStore = useAuthStore()
@ -81,6 +111,7 @@ const loading = ref(false)
const loginForm = reactive({
username: '',
password: '',
code: '',
rememberMe: false
})
@ -98,49 +129,38 @@ const loginRules = {
message: t('admin.login.passwordRequired'),
trigger: 'blur'
}
],
code: [
{
required: true,
message: t('admin.login.captchaRequired'),
trigger: 'blur'
}
]
}
// API
const mockLogin = (username, password) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
//
if (username === 'admin' && password === 'admin123') {
resolve({
token: 'mock-jwt-token',
user: {
id: 1,
username: 'admin',
name: 'Administrator',
role: 'admin'
}
})
} else {
reject(new Error('Invalid credentials'))
}
}, 1000)
})
}
const handleLogin = async () => {
try {
await loginFormRef.value.validate()
loading.value = true
const response = await mockLogin(loginForm.username, loginForm.password)
// 使store
authStore.login(response)
if (loginForm.rememberMe) {
localStorage.setItem('admin-remember-me', 'true')
//
const response = await requestUtils.common(clientApi.default.LOGIN, {
username: loginForm.username,
password: loginForm.password,
code: loginForm.code,
uuid: uuid.value
}
ElMessage.success(t('admin.login.loginSuccess'))
//
router.push('/admin')
)
if(response.code !== 200){
ElMessage.error(response.msg || t('admin.login.loginFailed'))
return
}
let data = response.data;
authStore.login(data,()=>{
router.push('/admin')
})
} catch (error) {
ElMessage.error(t('admin.login.loginFailed'))
console.error('Login error:', error)
@ -148,12 +168,23 @@ const handleLogin = async () => {
loading.value = false
}
}
const codeImg = ref('');
const uuid = ref('');
//
const getCaptchaCode = async () => {
PLUG.getCaptchaCode((data)=>{
codeImg.value = data.img;
console.log(codeImg.value);
uuid.value = data.uuid;
})
}
onMounted(() => {
//
if (authStore.checkAuth()) {
router.push('/admin')
return
}
getCaptchaCode();
})
</script>
@ -223,6 +254,46 @@ onMounted(() => {
box-shadow: 0 10px 25px -5px rgba(107, 70, 193, 0.3);
}
/* 验证码容器 */
.captcha-container {
display: flex;
gap: 12px;
align-items: center;
}
.captcha-input {
flex: 1;
}
.captcha-image {
width: 100px;
height: 40px;
border: 1px solid #dcdfe6;
border-radius: 6px;
cursor: pointer;
overflow: hidden;
transition: border-color 0.3s ease;
}
.captcha-image:hover {
border-color: #6b46c1;
}
.captcha-img {
width: 100%;
height: 100%;
object-fit: contain;
}
.captcha-loading {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #909399;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-box {
@ -233,6 +304,20 @@ onMounted(() => {
.login-title {
font-size: 24px;
}
.captcha-container {
flex-direction: column;
gap: 8px;
}
.captcha-input {
width: 100%;
}
.captcha-image {
width: 100%;
height: 50px;
}
}
@media (max-width: 480px) {
@ -243,5 +328,13 @@ onMounted(() => {
.login-title {
font-size: 20px;
}
.captcha-container {
gap: 6px;
}
.captcha-image {
height: 45px;
}
}
</style>

View File

@ -11,7 +11,6 @@
<div class="stat-label">{{ t('admin.disassemblyOrders.stats.total') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon pending">
<el-icon><Clock /></el-icon>

View File

@ -1,5 +1,5 @@
<template>
<div class="admin-users">
<div class="users">
<!-- 统计卡片 -->
<div class="user-stats">
<div class="stat-card">
@ -681,7 +681,7 @@ onMounted(() => {
</script>
<style scoped>
.admin-users {
.users {
padding: 20px;
}
@ -902,7 +902,7 @@ onMounted(() => {
/* 响应式设计 */
@media (max-width: 768px) {
.admin-users {
.users {
padding: 12px;
}

View File

@ -0,0 +1,4 @@
# 开发环境配置
VITE_BASE_URL=/api
VITE_DEV_MODE=true
VITE_LOG_LEVEL=info

View File

@ -1,11 +1,11 @@
# 生产环境配置
VITE_BASE_URL=https://api.deotaland.ai
# Vercel 部署环境变量配置示例
# 复制此文件为 .env.local 并填入实际值
# Google AI API Key用于 AI 功能)
VITE_GOOGLE_API_KEY=your_google_api_key_here
# 基础API地址
VITE_APP_BASE_API=https://your-api-domain.com/api
# Stripe 支付配置
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key

View File

@ -204,7 +204,6 @@ import {
Briefcase,
MagicStick,
Reading,
Setting
} from '@element-plus/icons-vue'
import { AGENT_STATUS, AGENT_ROLES, VOICE_MODELS } from '../stores/agents.js'
@ -230,7 +229,6 @@ export default {
Briefcase,
MagicStick,
Reading,
Setting
},
props: {
agent: {

View File

@ -162,7 +162,6 @@ import {
WarningFilled,
Warning,
User,
Setting,
Monitor,
Link,
CloseBold,
@ -212,7 +211,7 @@ const statusOptions = [
const roleOptions = [
{ value: 'all', label: '全部角色', icon: 'User' },
{ value: 'assistant', label: '助手', icon: 'Avatar' },
{ value: 'admin', label: '管理员', icon: 'Setting' },
{ value: 'admin', label: '管理员', icon: 'Avatar' },
{ value: 'support', label: '客服', icon: 'Connection' }
]

View File

@ -135,9 +135,8 @@ import demosst from '@/assets/demosst.png'
import emails from '../../assets/sketches/emails.png'
import { API_BASE_URL } from '../../../ipconfig';
import { computed, ref, onMounted, watch, nextTick } from 'vue';
import { request } from '../../utils/request'
import { getModelTaskStatus, generateCharacterImage, generateImageFromMultipleImages, generateFourViewSheet } from '../../services/aiService';
import { convertImagesToDataURL, convertImageToDataURL, isValidImagePath } from '../../utils/imageUtils';
import {generateImageFromMultipleImages, generateFourViewSheet } from '../../services/aiService';
import { convertImagesToDataURL, convertImageToDataURL } from '../../utils/imageUtils';
import promptConfig from '../../components/prompt.js'
// Element Plus
import { Cpu, View, Picture, ChatDotRound, MagicStick, CloseBold } from '@element-plus/icons-vue'
@ -347,12 +346,6 @@ const handleGenerateImage = async () => {
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(7);
const cacheBuster = `${timestamp}_${randomSuffix}`;
// profile
const modifiedProfile = {
...props?.cardData?.profile||{},
cacheBuster: cacheBuster
};
let imageUrl;
//
// if (props.cardData.sketch) {
@ -391,7 +384,7 @@ const handleGenerateImage = async () => {
const prompt = props.diyPromptText|| `A full-body character portrait
Ensure the output image has a portrait aspect ratio of 9:16.
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
角色特征Q 版萌系人物造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感色彩搭配和谐且富有层次材质表现逼真
角色特征Q 版萌系人物造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感色彩搭配和谐且富有层次眼睛位置与参考图一致材质表现逼真
${props?.cardData?.sketch?
`Ensure the IP character's body proportions strictly follow the proportions shown in the provided sketch; generate accordingly.`:``}
${props?.cardData?.selectedMaterial?
@ -423,7 +416,8 @@ const handleGenerateImage = async () => {
模型应呈现专业3D打印白模效果
Adjust the characters hairstyle to be thick, voluminous, and structurally robust with clear, solid contours, suitable for 3D printing. Ensure the hair has sufficient thickness and structural integrity to avoid fragility during the printing process, while retaining the original cute and stylized aesthetic. The textured details of the hair should be optimized for 3D manufacturingwith smooth yet distinct layers that are both visually appealing and printable, maintaining the overall whimsical and high-quality blind box character style.
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
图片不要有任何水印
图片不要有任何水印,
保证生成的任务图片一定要有眼睛
`
;
console.log('提示词构建',prompt);
@ -450,26 +444,7 @@ const handleGenerateImage = async () => {
console.error('生成角色图片失败:', error);
}
};
//
const generateSmoothWhiteModel = async (urlData) => {
console.log(urlData,'urlDataurlData');
try {
let imageUrl;
const referenceImages = [];
referenceImages.push(urlData);
const convertedImages = await convertImagesToDataURL(referenceImages);
const ipType = props?.cardData?.ipType||'人物';
const prompt = {
'人物':promptConfig.Personremovewhitemembranes,
'动物':promptConfig.Animalsremovewhitemembranes,
}[ipType];
imageUrl = await generateImageFromMultipleImages(convertedImages, prompt);
internalImageUrl.value = imageUrl;
// leftCardImageUrl.value = imageUrl;
} catch (error) {
console.error('生成角色图片失败:', error);
}
};
//
onMounted(async () => {
//

View File

@ -25,10 +25,7 @@
</div>
<div class="event-content">
<p class="event-description">{{ event.description }}</p>
<div v-if="event.location" class="event-location">
<el-icon><Location /></el-icon>
<span>{{ event.location }}</span>
</div>
<div v-if="event.operator" class="event-operator">
<el-icon><User /></el-icon>
<span>{{ event.operator }}</span>
@ -83,7 +80,6 @@
<script setup>
import { computed } from 'vue'
import {
Location,
User,
Van,
MapLocation,

View File

@ -85,7 +85,6 @@
<div class="order-details">
<div class="shipping-section">
<h4 class="section-title">
<el-icon><Location /></el-icon>
{{ t('orderManagement.order.shipping') }}
</h4>
<div class="shipping-info">
@ -202,7 +201,6 @@ import {
ArrowDown,
User,
Calendar,
Location,
CreditCard,
View,
Close,
@ -221,7 +219,6 @@ export default {
ArrowDown,
User,
Calendar,
Location,
CreditCard,
View,
Close,

View File

@ -56,13 +56,13 @@ import { useI18n } from 'vue-i18n'
import { ElIcon } from 'element-plus'
import {
CloseBold,
CreditCard,
InfoFilled,
CreditCard,
Document,
Calendar,
Setting,
Setting,
Picture,
Van,
InfoFilled
Van
} from '@element-plus/icons-vue'
const { t } = useI18n()

File diff suppressed because it is too large Load Diff

View File

@ -41,8 +41,9 @@
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
import { requestUtils,clientApi } from '@deotaland/utils'
const authStore = useAuthStore()
const { t } = useI18n()
const emit = defineEmits(['success', 'error'])
const props = defineProps({
loading: {
@ -72,21 +73,46 @@ const handleGoogleLogin = async () => {
if (isProcessing.value ) return
isProcessing.value = true
await loadGoogleScript()
const clientId = '680509991778-f5qgqbampabs1atblvm1jkoi4itl1nni.apps.googleusercontent.com'
// const clientId = '680509991778-f5qgqbampabs1atblvm1jkoi4itl1nni.apps.googleusercontent.com'
const clientId = '1087356879940-kcdvstc2pgoim27gffl67qrs297avdmb.apps.googleusercontent.com'
const callback = async (response) => {
console.log(response,'responseresponseresponseresponse');
const idToken = response && response.credential
if (!idToken) {
errorMessage.value = '未获取到 Google 身份凭证'
return
}
console.log(idToken,'idTokenidToken');
await loginWithidToken(idToken)
}
console.log('window.google');
// One Tap
window.google.accounts.id.initialize({ client_id: clientId, callback })
//
window.google.accounts.id.prompt()
}
const loginWithidToken = async (idToken) => {
try {clientApi
const res = await requestUtils.common(clientApi.default.OAUTH_GOOGLE,{
googleIdToken:idToken
})
if(res.code === 200){
// token
let data = res.data;
authStore.loginSuccess(data,()=>{
emit('success', res.data.user)
})
return
emit('success', res.data.user)
return res
}
return res
} catch (error) {
console.error('登录失败:', error)
emit('error', error)
throw error
} finally {
isProcessing.value = false
}
}
onMounted(() => {
loadGoogleScript()
})

View File

@ -255,15 +255,7 @@ const handleLogin = async () => {
font-size: 16px;
}
/* 错误消息 */
.error-message {
font-size: 13px;
color: #EF4444;
margin-left: 4px;
display: flex;
align-items: center;
gap: 4px;
}
/* 登录提交按钮 */
.login-submit-button {

View File

@ -22,7 +22,42 @@
</div>
<div v-if="emailError" class="error-message">{{ emailError }}</div>
</div>
<!-- 邮箱验证码输入 -->
<div class="form-group">
<label class="form-label" for="verificationCode">{{ t('register.verification_code_label') }}</label>
<div class="verification-wrapper">
<div class="input-wrapper" :class="{ 'focused': isCodeFocused }">
<input
id="verificationCode"
v-model="form.verificationCode"
type="text"
class="form-input verification-input"
:class="{ 'error': codeError }"
:placeholder="t('register.verification_code_placeholder')"
@focus="isCodeFocused = true"
@blur="validateVerificationCode"
:disabled="loading"
maxlength="6"
autocomplete="off"
/>
<div v-if="codeError" class="input-error-icon">
<el-icon><WarningFilled /></el-icon>
</div>
</div>
<button
type="button"
class="send-code-button"
@click="handleSendVerificationCode"
:disabled="isCodeSending"
:class="{ 'countdown-active': countdown > 0 }"
>
<span v-if="countdown > 0">{{ countdown }}s</span>
<span v-else-if="isCodeSending">{{ t('register.sending_code') }}</span>
<span v-else>{{ t('register.send_code') }}</span>
</button>
</div>
<div v-if="codeError" class="error-message">{{ codeError }}</div>
</div>
<!-- 密码输入 -->
<div class="form-group">
<label class="form-label" for="password">{{ t('register.password_label') }}</label>
@ -51,57 +86,6 @@
</div>
<div v-if="passwordError" class="error-message">{{ passwordError }}</div>
</div>
<!-- 确认密码输入 -->
<div class="form-group">
<label class="form-label" for="confirmPassword">{{ t('register.confirm_password_label') }}</label>
<div class="input-wrapper" :class="{ 'focused': isConfirmPasswordFocused }">
<input
id="confirmPassword"
v-model="form.confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
class="form-input"
:class="{ 'error': confirmPasswordError }"
:placeholder="t('register.confirm_password_placeholder')"
@focus="isConfirmPasswordFocused = true"
@blur="validateConfirmPassword"
:disabled="loading"
autocomplete="new-password"
/>
<button
type="button"
class="password-toggle"
@click="showConfirmPassword = !showConfirmPassword"
:disabled="loading"
>
<el-icon v-if="showConfirmPassword"><View /></el-icon>
<el-icon v-else><Hide /></el-icon>
</button>
</div>
<div v-if="confirmPasswordError" class="error-message">{{ confirmPasswordError }}</div>
</div>
<!-- 用户名输入 (可选) -->
<div class="form-group">
<label class="form-label" for="username">{{ t('register.username_label') }}</label>
<div class="input-wrapper" :class="{ 'focused': isUsernameFocused }">
<input
id="username"
v-model="form.username"
type="text"
class="form-input"
:class="{ 'error': usernameError }"
:placeholder="t('register.username_placeholder')"
@focus="isUsernameFocused = true"
@blur="validateUsername"
:disabled="loading"
autocomplete="username"
/>
<div v-if="usernameError" class="input-error-icon">
<el-icon><WarningFilled /></el-icon>
</div>
</div>
<div v-if="usernameError" class="error-message">{{ usernameError }}</div>
</div>
<!-- 注册按钮 -->
<button
@ -129,13 +113,12 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { WarningFilled, View, Hide } from '@element-plus/icons-vue'
import LOGIN from '../../views/Login/login'
const { t } = useI18n()
const loginPlugin = new LOGIN()
const emit = defineEmits(['success', 'error'])
const props = defineProps({
loading: {
@ -143,43 +126,80 @@ const props = defineProps({
default: false
}
})
const authStore = useAuthStore()
//
const form = ref({
email: '',
password: '',
confirmPassword: '',
username: ''
verificationCode: ''
})
//
const isEmailFocused = ref(false)
const isPasswordFocused = ref(false)
const isConfirmPasswordFocused = ref(false)
const isUsernameFocused = ref(false)
const isCodeFocused = ref(false)
const showPassword = ref(false)
const showConfirmPassword = ref(false)
const isCodeSending = ref(false)
const countdown = ref(0)
let countdownTimer = null
//
onBeforeUnmount(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
//
const emailError = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
const usernameError = ref('')
const codeError = ref('')
//
const isFormValid = computed(() => {
return form.value.email &&
form.value.password &&
form.value.confirmPassword &&
form.value.verificationCode &&
!emailError.value &&
!passwordError.value &&
!confirmPasswordError.value &&
(!form.value.username || !usernameError.value)
!codeError.value
})
//
const canSendCode = computed(() => {
return form.value.email && !emailError.value
})
//
const startCountdown = () => {
countdown.value = 60
countdownTimer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
} else {
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
}
//
const handleSendVerificationCode = async () => {
if (!canSendCode.value || isCodeSending.value) return
isCodeSending.value = true
try {
loginPlugin.sendEmailCode({
email:form.value.email,
purpose:'register'
},()=>{
startCountdown()
})
} catch (error) {
ElMessage.error(t('register.verification_code_send_failed'))
} finally {
isCodeSending.value = false
}
}
const validateEmail = () => {
if (!form.value.email) {
emailError.value = t('register.email_empty_error')
@ -196,78 +216,39 @@ const validatePassword = () => {
} else if (form.value.password.length < 8) {
passwordError.value = t('register.password_min_error')
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(form.value.password)) {
passwordError.value = t('register.password_strength_error')
passwordError.value = t('register.password_complexity_error')
} else {
passwordError.value = ''
}
//
validateConfirmPassword()
}
const validateConfirmPassword = () => {
if (!form.value.confirmPassword) {
confirmPasswordError.value = t('register.confirm_password_empty_error')
} else if (form.value.password !== form.value.confirmPassword) {
confirmPasswordError.value = t('register.password_mismatch_error')
const validateVerificationCode = () => {
if (!form.value.verificationCode) {
codeError.value = t('register.verification_code_empty_error')
} else if (!/^\d{6}$/.test(form.value.verificationCode)) {
codeError.value = t('register.verification_code_invalid_error')
} else {
confirmPasswordError.value = ''
}
}
const validateUsername = () => {
if (form.value.username) {
if (form.value.username.length < 3) {
usernameError.value = t('register.username_min_error')
} else if (!/^[a-zA-Z0-9_]+$/.test(form.value.username)) {
usernameError.value = t('register.username_invalid_error')
} else {
usernameError.value = ''
}
} else {
usernameError.value = ''
codeError.value = ''
}
}
//
watch(() => form.value.email, validateEmail)
watch(() => form.value.password, validatePassword)
watch(() => form.value.confirmPassword, validateConfirmPassword)
watch(() => form.value.username, validateUsername)
watch(() => form.value.verificationCode, validateVerificationCode)
//
const handleRegister = async () => {
//
validateEmail()
validatePassword()
validateConfirmPassword()
validateUsername()
if (!isFormValid.value) {
const handleRegister = () => {
//
if (!form.value.email || !form.value.password || !form.value.verificationCode) {
return
}
try {
const result = await authStore.register({
loginPlugin.confirmRegister({
email: form.value.email,
password: form.value.password,
username: form.value.username || undefined
emailCode: form.value.verificationCode,
password: form.value.password
},()=>{
// emit('success')
})
if (result.success) {
emit('success', result.user)
//
form.value.email = ''
form.value.password = ''
form.value.confirmPassword = ''
form.value.username = ''
} else {
emit('error', result.error)
}
} catch (error) {
const errorMessage = error.message || t('register.register_processing_error')
emit('error', errorMessage)
}
}
</script>
@ -383,6 +364,58 @@ const handleRegister = async () => {
gap: 4px;
}
/* 验证码输入组 */
.verification-wrapper {
display: flex;
gap: 12px;
align-items: flex-start;
}
.verification-input {
flex: 1;
}
.send-code-button {
height: 48px;
padding: 0 16px;
background: linear-gradient(135deg, #7C3AED, #6B46C1);
border: none;
border-radius: 12px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
min-width: 120px;
}
.send-code-button:hover:not(:disabled) {
background: linear-gradient(135deg, #8B5CF6, #7C3AED);
transform: translateY(-1px);
box-shadow:
0 8px 15px -3px rgba(167, 139, 250, 0.5),
0 4px 6px -2px rgba(167, 139, 250, 0.3);
}
.send-code-button:active:not(:disabled) {
transform: translateY(0);
box-shadow:
0 4px 6px -1px rgba(107, 70, 193, 0.3),
0 2px 4px -1px rgba(107, 70, 193, 0.2);
}
.send-code-button:disabled {
background: linear-gradient(135deg, #D1D5DB, #9CA3AF);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.send-code-button.countdown-active {
background: linear-gradient(135deg, #9CA3AF, #6B7280);
}
/* 注册提交按钮 */
.register-submit-button {
display: flex;
@ -493,6 +526,12 @@ const handleRegister = async () => {
font-size: 15px;
}
.send-code-button {
height: 44px;
font-size: 13px;
min-width: 100px;
}
.password-toggle {
right: 10px;
}
@ -510,6 +549,17 @@ const handleRegister = async () => {
font-size: 14px;
}
.send-code-button {
height: 42px;
font-size: 12px;
min-width: 90px;
padding: 0 12px;
}
.verification-wrapper {
gap: 8px;
}
.terms-notice {
padding: 12px;
}

View File

@ -332,12 +332,9 @@
<script setup>
import lts from '../../assets/sketches/lts.png'
import ltsby from '../../assets/sketches/ltsby.png'
import mk2dy from '../../assets/sketches/mk2dy.png'
import { ref, onMounted, watch, nextTick, computed } from 'vue';
import { request } from '../../utils/request.js';
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance } from 'vue';
import { optimizePrompt, } from '../../services/aiService.js';
import { ArrowDown } from '@element-plus/icons-vue';
import { ElMessage, ElLoading } from 'element-plus';
import cz1 from '../../assets/material/cz1.jpg'
import humanTypeImg from '../../assets/sketches/tcww.png'
@ -347,6 +344,10 @@ import { useRouter } from 'vue-router';
const emit = defineEmits(['image-generated', 'model-generated', 'generate-requested', 'import-character', 'navigate-back']);
const router = useRouter();
// i18n
const { proxy } = getCurrentInstance();
const $t = proxy.$t;
const openCharacterImport = () => {
emit('import-character');
};
@ -723,24 +724,19 @@ const handleSketchSelect = (sketch) => {
const validateGenerationInputs = () => {
const errors = []
const warnings = []
//
// if (!prompt.value || prompt.value.trim().length < 3) {
// errors.push('10')
// }
//
// -
if (!referenceImage.value && !selectedSketch.value) {
warnings.push('建议上传参考图像或选择草图以获得更好的生成效果')
errors.push($t('common.validation.referenceImageRequired'))
}
return { errors, warnings }
}
//
const handleValidationError = (errors) => {
if (errors.length === 1) {
} else {
if (errors.length > 0) {
// i18n
const errorMessage = typeof errors[0] === 'string' ? errors[0] : String(errors[0])
ElMessage.error(errorMessage)
}
}
@ -873,11 +869,6 @@ const handleGenerate = async () => {
handleValidationError(validation.errors);
return;
}
//
if (validation.warnings.length > 0) {
validation.warnings.forEach(warning => {
});
}
try {
await handleGenerateWithMultipleImages();
// //

View File

@ -36,7 +36,7 @@
</button> -->
<!-- 通知按钮 -->
<button
<!-- <button
class="action-button notification-button"
:aria-label="t('header.notifications')"
@click="toggleNotifications"
@ -45,7 +45,7 @@
<span v-if="notificationCount > 0" class="notification-badge">
{{ notificationCount }}
</span>
</button>
</button> -->
<!-- 用户菜单 -->
<div class="user-menu" v-if="currentUser">
@ -64,7 +64,6 @@
{{ t('header.profile') }}
</el-dropdown-item>
<el-dropdown-item command="settings">
<SettingsIcon class="dropdown-item-icon" />
{{ t('header.settings') }}
</el-dropdown-item>
<el-dropdown-item divided command="logout">
@ -86,7 +85,7 @@
<!-- 移动端用户操作 -->
<div v-if="isMobile && currentUser" class="mobile-user-menu">
<el-dropdown trigger="click" @command="handleUserCommand">
<el-avatar :size="32" :src="currentUser.avatar" class="mobile-avatar">
<el-avatar :size="32" :src="currentUser.avatarUrl" class="mobile-avatar">
<el-icon><UserIcon /></el-icon>
</el-avatar>
<template #dropdown>
@ -96,11 +95,9 @@
{{ t('header.profile') }}
</el-dropdown-item>
<el-dropdown-item command="settings">
<SettingsIcon class="dropdown-item-icon" />
{{ t('header.settings') }}
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<LogoutIcon class="dropdown-item-icon" />
{{ t('header.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
@ -184,7 +181,6 @@ import {
Search as SearchIcon,
Bell as NotificationIcon,
User as UserIcon,
Setting as SettingsIcon,
Right as LogoutIcon,
ArrowDown as ChevronDownIcon,
Close as XIcon,
@ -200,7 +196,6 @@ export default {
SearchIcon,
NotificationIcon,
UserIcon,
SettingsIcon,
LogoutIcon,
ChevronDownIcon,
XIcon,

View File

@ -17,7 +17,6 @@
<CreationIcon v-else-if="item.icon === 'CreationIcon'" />
<GalleryIcon v-else-if="item.icon === 'GalleryIcon'" />
<OrdersIcon v-else-if="item.icon === 'OrdersIcon'" />
<SettingsIcon v-else-if="item.icon === 'SettingsIcon'" />
</div>
<transition name="fade">
<span v-if="!collapsed" class="nav-text">{{ item.label }}</span>
@ -70,7 +69,6 @@ import {
Folder as CreationIcon,
Picture as GalleryIcon,
ShoppingCart as OrdersIcon,
Setting as SettingsIcon,
User as UserIcon,
Document as DocumentIcon,
VideoPlay as VideoIcon,
@ -89,7 +87,6 @@ export default {
CreationIcon,
GalleryIcon,
OrdersIcon,
SettingsIcon,
UserIcon,
DocumentIcon,
VideoIcon,
@ -152,13 +149,6 @@ export default {
icon: 'OrdersIcon',
badge: null
},
// {
// id: 'device-settings',
// path: '/device-settings',
// label: t('sidebar.deviceSettings'),
// icon: 'SettingsIcon',
// badge: null
// }
])
//

View File

@ -113,13 +113,10 @@
<script setup>
import { computed, ref, onMounted } from 'vue';
import ThreeModelViewer from '../ThreeModelViewer/index.vue';
// AI
import { createModelTask, getModelTaskStatus } from '../../services/aiService.js';
//
import { convertImageArrayToFormData } from '../../utils/imageUtils.js';
import { MeshyServer } from '@deotaland/utils';
//
const emit = defineEmits(['clickModel','refineModel']);
const Meshy = new MeshyServer();
//
const showRightControls = ref(false);
const threeModelViewer = ref(null);
@ -140,182 +137,39 @@ const handleCardClick = () => {
emit('clickModel', modelData);
};
const modelData = ref({});
const handleRefineModel = ()=>{
emit('refineModel',modelData.value.task_id);
}
//
const handleGenerateModel = async () => {
isGenerating.value = true;
progressPercentage.value = 0;
try {
let modelImage = props.imageUrl;
if(props.generateFourView){
//
const fourViewImages = await splitFourViewImage(modelImage);
// FormDatabase64
modelImage = convertImageArrayToFormData(fourViewImages);
}
// AI
const taskResult = await createModelTask({
image_url: modelImage,
generate_four_view: props.generateFourView,
refine_model: props.refineModel,
refine_model_task_id: props.refineModelTaskId,
});
modelData.value = taskResult;
console.log(taskResult,'taskResulttaskResulttaskResult');
if (taskResult && taskResult.task_id) {
// 使
pollTaskStatus(taskResult.task_id);
} else {
throw new Error('创建模型任务失败');
}
} catch (error) {
console.error('生成模型失败:', error);
isGenerating.value = false;
progressPercentage.value = 0;
}
};
//
const pollTaskStatus = async (taskId) => {
try {
const result = await getModelTaskStatus(taskId, props.generateFourView);
console.log(result, 'resultresult');
if (result && result.status) {
if (result.status === 'success') {
//
console.log('模型生成完成!');
progressPercentage.value = 100;
// 100%
setTimeout(() => {
Meshy.createModelTask({
project_id: 0,
image_url: props.imageUrl,
},(result)=>{
if(result){
Meshy.getModelTaskStatus(result,(modelUrl)=>{
if(modelUrl){
//
generatedModelUrl.value = modelUrl;
isGenerating.value = false;
progressPercentage.value = 100;
}
},(error)=>{
console.error('模型生成失败:', error);
isGenerating.value = false;
// URL
generatedModelUrl.value = result.output?.pbr_model || result.output?.model;
taskStatus.value = 'success';
}, 500);
} else if (result.status === 'failed') {
//
console.error('模型生成失败: ' + (result.error || '未知错误'));
isGenerating.value = false;
taskStatus.value = 'failed';
progressPercentage.value = 0;
} else if (result.status === 'running') {
// 使API
if (result.progress !== undefined) {
progressPercentage.value = Math.max(0, Math.min(100, result.progress));
}
//
setTimeout(() => {
pollTaskStatus(taskId);
}, 2000); // 2
} else {
//
setTimeout(() => {
pollTaskStatus(taskId);
}, 2000);
progressPercentage.value = 0;
},(progress)=>{
if (progress !== undefined) {
progressPercentage.value = progress;
}
})
}
} else {
throw new Error('无效的响应数据');
}
} catch (error) {
console.error('轮询任务状态失败:', error);
//
setTimeout(() => {
pollTaskStatus(taskId);
}, 5000); // 5
}
})
};
//
const splitFourViewImage = async (imageUrl) => {
return new Promise((resolve, reject) => {
// Image
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
// canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
//
const originalWidth = img.naturalWidth;
const originalHeight = img.naturalHeight;
// 2x2
const subWidth = originalWidth / 2;
const subHeight = originalHeight / 2;
// canvas
canvas.width = subWidth;
canvas.height = subHeight;
//
const positions = [
{ x: 0, y: 0, name: 'front' }, // -
{ x: 0, y: subHeight, name: 'left' }, // -
{ x: subWidth, y: 0, name: 'back' }, // -
{ x: subWidth, y: subHeight, name: 'right' } // -
];
const fourViewImages = [];
positions.forEach((pos) => {
// canvas
ctx.clearRect(0, 0, subWidth, subHeight);
// canvas
ctx.drawImage(
img,
pos.x, pos.y, subWidth, subHeight, //
0, 0, subWidth, subHeight // canvas
);
// DataURL
const dataURL = canvas.toDataURL('image/png');
fourViewImages.push({
type: "png",
url: dataURL,
viewName: pos.name //
});
});
//
const orderedImages = [
fourViewImages.find(img => img.viewName === 'front'), //
fourViewImages.find(img => img.viewName === 'left'), //
fourViewImages.find(img => img.viewName === 'back'), //
fourViewImages.find(img => img.viewName === 'right') //
].map(img => ({
type: img.type,
url: img.url
}));
resolve(orderedImages);
} catch (error) {
reject(new Error(`切割四视图失败: ${error.message}`));
}
};
img.onerror = () => {
reject(new Error('加载四视图图片失败'));
};
//
img.src = imageUrl;
});
};
// URLURL
onMounted(() => {
//
setTimeout(() => {
isInitializing.value = false;
}, 1000);
console.log(props,'参数');
if(false){//
return
}
handleGenerateModel();
});
@ -329,41 +183,6 @@ const handleMouseLeave = () => {
//
showRightControls.value = false;
};
// /
const toggleActions = () => {
showRightControls.value = !showRightControls.value;
};
// GLB
const exportAsGLB = () => {
if (threeModelViewer.value) {
threeModelViewer.value.exportAsGLB();
}
};
// OBJ
const exportAsOBJ = () => {
if (threeModelViewer.value) {
threeModelViewer.value.exportAsOBJ();
}
};
// STL
const exportAsSTL = () => {
if (threeModelViewer.value) {
threeModelViewer.value.exportAsSTL();
}
};
//
const exportModel = () => {
if (threeModelViewer.value) {
// GLB
threeModelViewer.value.exportAsGLB();
}
};
//
const handleImageLoad = (event) => {
const img = event.target;
@ -379,37 +198,6 @@ const handleImageLoad = (event) => {
const handleImageError = (event) => {
console.warn('图片加载失败:', event.target.src);
};
//
const toggleRotation = () => {
isRotating.value = !isRotating.value;
};
//
const resetView = () => {
// 3D
};
//
const changeModelColor = () => {
// 3D
};
// 线
const toggleWireframe = () => {
// 线
};
//
const changeLighting = () => {
//
};
//
const showExportOptions = () => {
//
};
//
const props = defineProps({
// URL
@ -452,26 +240,11 @@ const props = defineProps({
type: Number,
default: 8
},
//
cardName: {
type: String,
default: 'Chrono Warden 801'
},
//
cardSeries: {
type: String,
default: 'Series: Quantum Nights'
},
//
clickDisabled: {
type: Boolean,
default: false
},
// 使9:16false使
useFixedRatio: {
type: Boolean,
default: false
},
//
generateFourView: {
type: Boolean,
@ -491,8 +264,6 @@ const props = defineProps({
// URL
const generatedModelUrl = ref('');
//
const taskStatus = ref('');
//
const imageAspectRatio = ref(16 / 9); //

View File

@ -52,54 +52,5 @@ export default {
10. **风格** 整体风格应是干净简约的3D渲染专注于提供一个可用于后续设计和定制的空白画布"
`,
//发型脱离
Hairseparation:`1. Layout & Composition
Arrange all separated parts (body, hairstyle, headwear/accessories) horizontally in a single image, with clear spacing between each part.
Ensure no overlapping or occlusion, and all parts are fully visible.
Maintain correct scale and proportional alignment between parts for 3D modeling reference.
2. Body
Include torso, limbs, clothing, and accessories, excluding head, hair, and headwear.
Preserve all clothing folds, decorations, and accessory details.
Output as solid, smooth 3D printable model, suitable for resin printing.
3. Hairstyle
Display the hairstyle alone, fully detached from body.
Preserve full 3D volume, flow, and original design, with slightly thicker strands and simplified geometry for durability.
Ensure no cropping or obstruction by other parts.
4. Headwear / Accessories
Separate hat, headpiece, or other head accessories, displayed in-line with hair and body.
Maintain correct scale and alignment relative to hair.
Fully visible, no overlaps.
5. Rendering & Surface Requirements
Output as solid, sculpture-like models, no colors, textures, or transparency.
Surfaces: smooth, matte, clean, like unpainted resin prototype.
Background: neutral or pure white.
Perspective: slightly angled front view, clearly showing depth and shape of each part.
6. Additional Instructions for AI
Emphasize horizontal arrangement, isolation, and even spacing.
Highlight structural continuity, 3D volume, and printable geometry.
Avoid tiny fragile details or intersections that may break or obscure parts.
`,
Hairseparation:``,
}

View File

@ -192,7 +192,10 @@ export default {
defaultName: '创作者',
subtitle: '今天想创作什么精彩内容?',
greetingMessage: '欢迎回到你的创意空间,今天想创造什么呢?',
startCreating: '开始创作'
startCreating: '开始创作',
loginToStart: '登录开始创作',
register: '免费注册',
clickToLogin: '点击登录'
},
stats: {
creations: '创作作品',
@ -792,7 +795,7 @@ export default {
},
forgotPassword: {
title: '重置密码',
subtitle: '输入您的邮箱地址,我们将发送重置密码的链接',
subtitle: '输入您的邮箱地址,我们将发送验证码来重置您的密码',
back_to_login: '返回登录',
remember_password: '记起密码了?',
login_now: '立即登录',
@ -802,6 +805,37 @@ export default {
language_toggle_tooltip: '切换到英文',
email_label: '邮箱地址',
email_placeholder: '请输入邮箱地址',
verification_code_label: '验证码',
verification_code_placeholder: '请输入6位验证码',
verification_hint: '请输入6位数字验证码',
send_code: '发送验证码',
sending_code: '发送中...',
new_password_label: '新密码',
new_password_placeholder: '请输入新密码',
confirm_password_label: '确认新密码',
confirm_password_placeholder: '请再次输入新密码',
reset_password: '重置密码',
resetting: '重置中...',
email_empty_error: '请输入邮箱地址',
email_invalid_error: '请输入有效的邮箱地址',
verification_code_empty_error: '请输入验证码',
verification_code_invalid_error: '请输入有效的6位验证码',
new_password_empty_error: '请输入新密码',
new_password_min_error: '新密码至少需要6个字符',
confirm_password_empty_error: '请确认新密码',
confirm_password_mismatch_error: '两次输入的密码不一致',
verification_code_sent: '验证码发送成功',
verification_code_send_failed: '验证码发送失败',
reset_processing_error: '重置密码过程中发生错误',
resend_after: '重新发送',
resend_code: '重新发送',
reset_success: '密码重置成功',
reset_success_message: '您的密码已成功重置,请使用新密码登录',
password_weak: '弱',
password_fair: '一般',
password_good: '良好',
password_strong: '强',
password_very_strong: '非常强'
},
register: {
title: '创建账号',
@ -817,10 +851,10 @@ export default {
email_placeholder: '请输入邮箱地址',
password_label: '密码',
password_placeholder: '请输入密码',
confirm_password_label: '确认密码',
confirm_password_placeholder: '请确认密码',
username_label: '用户名',
username_placeholder: '请输入用户名',
verification_code_label: '验证码',
verification_code_placeholder: '请输入6位验证码',
send_code: '发送验证码',
sending_code: '发送中...',
register_button: '注册',
registering: '注册中...',
terms_agreement: '注册即表示您同意我们的',
@ -832,10 +866,10 @@ export default {
password_empty_error: '请输入密码',
password_min_error: '密码至少需要6个字符',
password_strength_error: '密码必须包含字母和数字',
confirm_password_empty_error: '请确认密码',
password_mismatch_error: '两次输入的密码不一致',
username_min_error: '用户名至少需要3个字符',
username_invalid_error: '用户名只能包含字母、数字和下划线',
verification_code_empty_error: '请输入验证码',
verification_code_invalid_error: '请输入有效的6位验证码',
verification_code_sent: '验证码发送成功',
verification_code_send_failed: '验证码发送失败',
register_processing_error: '注册过程中发生错误'
},
common: {
@ -847,7 +881,10 @@ export default {
generate: '生成',
back: '返回',
save: '保存',
create: '创建'
create: '创建',
validation: {
referenceImageRequired: '请上传参考图像或选择草图以继续生成'
}
}
},
en: {
@ -879,7 +916,7 @@ export default {
},
forgotPassword: {
title: 'Reset Password',
subtitle: 'Enter your email address and we will send you a password reset link',
subtitle: 'Enter your email address and we will send you a verification code to reset your password',
back_to_login: 'Back to Login',
remember_password: 'Remember your password?',
login_now: 'Login Now',
@ -889,17 +926,37 @@ export default {
language_toggle_tooltip: 'Switch to Chinese',
email_label: 'Email Address',
email_placeholder: 'Enter your email',
verification_code_label: 'Verification Code',
verification_code_placeholder: 'Enter 6-digit verification code',
verification_hint: 'Please enter 6-digit numeric verification code',
send_code: 'Send Code',
sending_code: 'Sending...',
new_password_label: 'New Password',
new_password_placeholder: 'Enter new password',
confirm_password_label: 'Confirm New Password',
confirm_password_placeholder: 'Enter new password again',
reset_password: 'Reset Password',
resetting: 'Resetting...',
email_empty_error: 'Please enter email address',
email_invalid_error: 'Please enter a valid email address',
send_reset_email: 'Send Reset Email',
sending: 'Sending...',
email_sent_title: 'Email Sent Successfully',
email_sent_description: 'We have sent a password reset link to your email, please check your inbox',
email_not_received: "Haven't received the email?",
resend_after: 'Resend after',
resend_email: 'Resend Email',
verification_code_empty_error: 'Please enter verification code',
verification_code_invalid_error: 'Please enter a valid 6-digit verification code',
new_password_empty_error: 'Please enter new password',
new_password_min_error: 'New password must be at least 6 characters',
confirm_password_empty_error: 'Please confirm new password',
confirm_password_mismatch_error: 'Passwords do not match',
verification_code_sent: 'Verification code sent successfully',
verification_code_send_failed: 'Failed to send verification code',
reset_processing_error: 'An error occurred during password reset processing',
resend_processing_error: 'An error occurred during email resending processing'
resend_after: 'Resend after',
resend_code: 'Resend Code',
reset_success: 'Password Reset Successful',
reset_success_message: 'Your password has been successfully reset, please login with your new password',
password_weak: 'Weak',
password_fair: 'Fair',
password_good: 'Good',
password_strong: 'Strong',
password_very_strong: 'Very Strong'
},
sidebar: {
dashboard: 'Dashboard',
@ -1069,7 +1126,10 @@ export default {
defaultName: 'Creator',
subtitle: 'What amazing content will you create today?',
greetingMessage: 'Welcome back to your creative space, what would you like to create today?',
startCreating: 'Start Creating'
startCreating: 'Start Creating',
loginToStart: 'Login to Start Creating',
register: 'Free Register',
clickToLogin: 'Click to Login'
},
stats: {
creations: 'Creations',
@ -1343,10 +1403,10 @@ export default {
email_placeholder: 'Enter your email',
password_label: 'Password',
password_placeholder: 'Enter your password',
confirm_password_label: 'Confirm Password',
confirm_password_placeholder: 'Confirm your password',
username_label: 'Username',
username_placeholder: 'Enter your username',
verification_code_label: 'Verification Code',
verification_code_placeholder: 'Enter 6-digit verification code',
send_code: 'Send Code',
sending_code: 'Sending...',
register_button: 'Register',
registering: 'Registering...',
terms_agreement: 'By registering, you agree to our ',
@ -1358,10 +1418,10 @@ export default {
password_empty_error: 'Please enter password',
password_min_error: 'Password must be at least 6 characters',
password_strength_error: 'Password must contain letters and numbers',
confirm_password_empty_error: 'Please confirm your password',
password_mismatch_error: 'Passwords do not match',
username_min_error: 'Username must be at least 3 characters',
username_invalid_error: 'Username can only contain letters, numbers and underscores',
verification_code_empty_error: 'Please enter verification code',
verification_code_invalid_error: 'Please enter a valid 6-digit code',
verification_code_sent: 'Verification code sent successfully',
verification_code_send_failed: 'Failed to send verification code',
register_processing_error: 'An error occurred during registration processing'
},
payment: {
@ -1694,7 +1754,10 @@ export default {
generate: 'Generate',
back: 'Back',
save: 'Save',
create: 'Create'
create: 'Create',
validation: {
referenceImageRequired: 'Please upload a reference image or select a sketch to continue generation'
}
}
},
},

View File

@ -87,81 +87,11 @@ scrollbarStyle.textContent = `
}
`
// Force override any CSS that might affect message position and styling
const style = document.createElement('style')
style.textContent = `
.el-message {
position: fixed !important;
top: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 9999 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 8px 16px !important;
border-radius: 6px !important;
}
.el-message .el-message__content {
display: inline-block !important;
vertical-align: middle !important;
margin: 0 !important;
padding: 0 !important;
line-height: 1.4 !important;
font-size: 14px !important;
}
.el-message .el-message__icon {
display: inline-block !important;
vertical-align: middle !important;
margin-right: 8px !important;
margin-left: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.el-message .el-message__group {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
}
.el-message .el-message__closeBtn {
margin-left: 8px !important;
vertical-align: middle !important;
}
html.dark .el-message {
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #f3f4f6 !important;
}
html.dark .el-message--success .el-message__icon {
color: #10b981 !important;
}
html.dark .el-message--error .el-message__icon {
color: #ef4444 !important;
}
html.dark .el-message--warning .el-message__icon {
color: #f59e0b !important;
}
@media (max-width: 768px) {
.el-message {
top: 10px !important;
left: 10px !important;
right: 10px !important;
transform: none !important;
padding: 6px 12px !important;
}
}
`
document.head.appendChild(style)
// Element Plus ElMessage uses default styles - no custom overrides needed
document.head.appendChild(scrollbarStyle)
// Import message-fix styles last to ensure project dark overrides don't break
// Element Plus ElMessage appearance.
import './styles/element-fix.css'
app.mount('#app')

View File

@ -1,4 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory} from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const ModernHome = () => import('../views/ModernHome.vue')
@ -21,7 +21,7 @@ const routes = [
path: '/',
name: 'home',
component: ModernHome,
meta: { requiresAuth: true, keepAlive: false }
meta: { requiresAuth: false, keepAlive: false }
},
{
path: '/ui-test',
@ -120,49 +120,26 @@ const router = createRouter({
// 路由守卫
router.beforeEach(async (to, from, next) => {
next()
return
const authStore = useAuthStore()
// 获取当前用户状态
const isAuthenticated = authStore.isAuthenticated
const currentUser = authStore.user
// 检查是否需要认证
if (to.meta.requiresAuth && !isAuthenticated) {
// 需要认证但未登录,跳转到登录页面
next({
name: 'login',
query: { redirect: to.fullPath }
})
return
}
// 检查是否只允许访客访问(如登录页面)
if (to.meta.requiresGuest && isAuthenticated) {
// 已登录用户访问登录页面,跳转到主页
next({ name: 'home' })
return
}
// 角色权限检查(如果路由指定了需要的角色)
if (to.meta.requiredRole && currentUser) {
const roleHierarchy = {
'viewer': 1,
'admin': 2,
'creator': 3
}
const userLevel = roleHierarchy[currentUser.role] || 0
const requiredLevel = roleHierarchy[to.meta.requiredRole] || 0
if (userLevel < requiredLevel) {
// 权限不足,跳转到无权限页面或主页
next({ name: 'home' })
window.localStorage.setItem('token','123')
return next()
// 检查是否需要登录
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
// 如果没有 token跳转到登录页
if (!token) {
next('/login')
return
}
}
// 检查是否需要游客身份
if (to.meta.requiresGuest) {
const authStore = useAuthStore()
// 如果有 token跳转到首页
if (authStore.token) {
next('/')
return
}
}
// 所有检查通过,允许访问
next()
})

View File

@ -1,9 +1,6 @@
import { GoogleGenAI, Type, GenerateContentResponse, Modality } from "@google/genai";
import { request } from '../utils/request.js';
import { GoogleGenAI, Type, Modality } from "@google/genai";
import { createLogger, logApiRequest, logPerformance } from '../utils/logger.js';
import { withRetry, handleError, AppError, ERROR_TYPES } from '../utils/errorHandler.js';
import { API_BASE_URL } from '../../ipconfig.js';
import modelConfig from './modelAiconfig.js';
/**
* AI服务模块
* 提供完整的AI功能包括文本优化角色生成等
@ -176,116 +173,7 @@ const handleAIError = async (error, context = '') => {
// ==================== 核心AI服务函数 ====================
/**
* 生成3D模型
* @param {string} imageUrl - 图片URL
* @param {Object} options - 生成选项
* @returns {Promise<Object>} 生成结果
*/
export const generate3DModel = async (imageUrl, options = {}) => {
const startTime = Date.now();
logger.info('Starting 3D model generation', { imageUrl, options });
try {
// 输入验证
if (!imageUrl) {
throw new AppError('请提供图片URL', ERROR_TYPES.VALIDATION, 'low');
}
// 构建请求参数
const params = {
model_version: options.modelVersion || 'v2.5-20250123',
model_seed: options.modelSeed,
face_limit: options.faceLimit,
texture: options.texture !== false,
pbr: options.pbr !== false,
texture_seed: options.textureSeed,
texture_alignment: options.textureAlignment || 'original_image',
texture_quality: options.textureQuality || 'standard',
auto_size: options.autoSize || false,
style: options.style,
orientation: options.orientation || 'default',
quad: options.quad || false,
compress: options.compress,
smart_low_poly: options.smartLowPoly || false,
generate_parts: options.generateParts || false,
geometry_quality: options.geometryQuality,
image_url: imageUrl
};
// 发送请求
const response = await withRetry(
async () => {
return await request.post('/api/v1/image-to-model', params);
},
AI_CONFIG.retryAttempts,
AI_CONFIG.retryDelay
);
// 记录性能
logPerformance('generate3DModel', startTime);
// 记录API请求
logApiRequest('/api/v1/image-to-model', 'POST', params, response);
// 返回结果
return {
taskId: response.taskId,
status: response.status,
message: response.message
};
} catch (error) {
// 错误处理
throw await handleAIError(error, 'generate3DModel');
}
};
/**
* 查询3D模型生成状态
* @param {string} taskId - 任务ID
* @returns {Promise<Object>} 任务状态
*/
export const get3DModelStatus = async (taskId) => {
const startTime = Date.now();
logger.info('Checking 3D model generation status', { taskId });
try {
// 输入验证
if (!taskId) {
throw new AppError('请提供任务ID', ERROR_TYPES.VALIDATION, 'low');
}
// 发送请求
const response = await withRetry(
async () => {
return await request.get(`/api/v1/models/${taskId}`);
},
AI_CONFIG.retryAttempts,
AI_CONFIG.retryDelay
);
// 记录性能
logPerformance('get3DModelStatus', startTime);
// 记录API请求
logApiRequest(`/api/v1/models/${taskId}`, 'GET', null, response);
// 返回结果
return {
taskId: response.taskId,
status: response.status,
modelUrl: response.modelUrl,
thumbnailUrl: response.thumbnailUrl,
parts: response.parts || [],
createdAt: response.createdAt
};
} catch (error) {
// 错误处理
throw await handleAIError(error, 'get3DModelStatus');
}
};
/**
* 生成3D模型并轮询状态直到完成
@ -1429,202 +1317,9 @@ export const generateCharacterImage = async (profile, style, inspirationImage =
throw new Error(aiError.getUserMessage());
}
};
/**
* 查询模型生成任务状态
* @param {string} taskId - 任务ID
* @returns {Promise<Object>} 任务状态信息
*/
export const getModelTaskStatus = async (taskId,generateFourView = false) => {
// 输入验证
if (!taskId || !taskId.trim()) {
throw new Error('请提供任务ID');
}
// 轮询检查任务状态
while (true) {
try {
const response = await request.post(`${API_BASE_URL}/api/v1/tasks/status`,
{
taskId: taskId,
type: modelConfig.model,
generateFourView:generateFourView
},
{
timeout: 10000 // 10秒超时
});
const taskData = response.data;
const status = taskData.status;
// 如果任务成功或者正在进行中,返回结果
if (status === 'success'||status === 'running') {
return taskData;
}
// 如果任务失败,抛出错误
if (status === 'failed' || status === 'error') {
throw new Error(taskData.data?.error || taskData.error || '任务执行失败');
}
// 等待3秒后继续轮询
await new Promise(resolve => setTimeout(resolve, 3000));
} catch (error) {
// 如果是网络错误,等待后重试
if (error.code === 'ECONNABORTED' || error.code === 'NETWORK_ERROR') {
await new Promise(resolve => setTimeout(resolve, 3000));
continue;
}
// 其他错误直接抛出
throw error;
}
}
};
// ==================== 健康检查 ====================
/**
* 检查AI服务健康状态
*/
export const healthCheck = async () => {
const startTime = Date.now();
try {
// 简单的健康检查请求
const response = await request.get('/ai/health', {
timeout: 5000
});
const responseTime = Date.now() - startTime;
return {
success: true,
status: 'healthy',
responseTime,
timestamp: new Date().toISOString(),
cacheStats: getCacheStats()
};
} catch (error) {
logger.error('Health check failed', error);
return {
success: false,
status: 'unhealthy',
error: error.message,
responseTime: Date.now() - startTime,
timestamp: new Date().toISOString()
};
}
};
// ==================== 模型任务创建与轮询 ====================
/**
* 创建模型生成任务
* @param {Object} params - 任务参数
* @param {string} params.image_url - 图片URL
* @param {string} params.prompt - 提示词
* @returns {Promise<Object>} 任务创建结果包含task_id
*/
export const createModelTask = async (params) => {
console.log(params,'paramsparamsparamsparams');
let uploadResponse = null
if(params.generate_four_view){
if(modelConfig.model=='meshy'){
uploadResponse = await request.post(`${API_BASE_URL}/api/v1/meshy/fourmodel`, params.image_url);
}else{
uploadResponse = await request.post(`${API_BASE_URL}/api/v1/upload/fourmodel`, params.image_url);
}
}else if(params.refine_model){
uploadResponse = await request.post(`${API_BASE_URL}/api/v1/upload/refine`, {
task_id: params.refine_model_task_id,
});
}else{//单视图模型
const response = await fetch(params.image_url);
const blob = await response.blob();
// 创建FormData对象
const formData = new FormData();
formData.append('image', blob, 'model-image.png');
// 发送上传请求
if(modelConfig.model=='meshy'){
uploadResponse = await request.post(`${API_BASE_URL}/api/v1/meshy/create_model`, formData);
}else if(modelConfig.model=='hitem3d'){
uploadResponse = await request.post(`${API_BASE_URL}/api/v1/hitem3d/create_model`, formData);
}
else{
uploadResponse = await request.post(`${API_BASE_URL}/api/v1/upload`, formData);
}
}
console.log(uploadResponse,'uploadResponseuploadResponseuploadResponse');
if (uploadResponse && uploadResponse.task_id) {
return uploadResponse
}
};
/**
* 轮询模型生成任务状态
* @param {string} taskId - 任务ID
* @returns {Promise<Object>} 任务状态信息
*/
export const pollModelTask = async (taskId) => {
const startTime = Date.now();
logger.info('Polling model generation task status', { taskId });
try {
// 输入验证
if (!taskId || !taskId.trim()) {
throw new AppError('请提供任务ID', ERROR_TYPES.VALIDATION, 'high');
}
// 发送请求到后端API
const result = await withRetry(async () => {
const requestStart = Date.now();
const response = await request.get(`${API_BASE_URL}/api/v1/tasks/${taskId}/status`, {
timeout: 10000, // 10秒超时
});
const requestDuration = Date.now() - requestStart;
logApiRequest('Model', 'pollTask', response.status, requestDuration, {
taskId
});
if (!response.data.success) {
throw new Error(response.data.error || '查询任务状态失败');
}
return response.data.data;
}, {
maxAttempts: AI_CONFIG.retryAttempts,
baseDelay: AI_CONFIG.retryDelay
});
const processingTime = Date.now() - startTime;
// 记录性能指标
logPerformance('pollModelTask', processingTime, {
taskId,
status: result.status,
cacheHit: false
});
logger.info('Model generation task status retrieved successfully', {
processingTime,
taskId,
status: result.status
});
return result;
} catch (error) {
const aiError = await handleAIError(error, 'pollModelTask');
// 记录失败的性能指标
logPerformance('pollModelTask', Date.now() - startTime, {
success: false,
errorType: aiError.type,
taskId
});
// 抛出错误,让调用方处理
throw new Error(aiError.getUserMessage());
}
};
/**
* 从多个参考图片生成新图片
* 支持多图像融合可以将不同的图像组合成一个无缝的新视觉效果

View File

@ -1,4 +1,3 @@
import { request as api } from '../utils/request.js'
/**
* 支付服务类

View File

@ -1,24 +1,22 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { request } from '../utils/request'
import { clientApi, requestUtils } from '@deotaland/utils'
export const useAuthStore = defineStore('auth', () => {
// 状态定义
const user = ref(null)
const user = ref({})
const token = ref('')
const loading = ref(false)
// 登录方法
const login = async (data) => {
const login = async (data,callback=null) => {
loading.value = true
try {
const res = await request.common(request.url.LOGIN, data)
const res = await requestUtils.common(clientApi.default.LOGIN, data)
if(res.code === 200){
let data = res.data;
// 登录成功保存token和用户信息
token.value = res.data.token
user.value = res.data.user
localStorage.setItem('token', res.data.token)
loginSuccess(data,callback)
return res
}
return res
} catch (error) {
console.error('登录失败:', error)
throw error
@ -26,16 +24,24 @@ export const useAuthStore = defineStore('auth', () => {
loading.value = false
}
}
//登录成功方法
const loginSuccess = (data,callback=null) => {
token.value = data.accessToken
user.value = data
localStorage.setItem('token', token.value);
callback&&callback();
}
// 登出方法
const logout = async () => {
const logout = async (callback) => {
loading.value = true
try {
const res = await request.common(request.url.LOGOUT)
const res = await requestUtils.common(clientApi.default.LOGOUT)
if(res.code === 200){
// 登出成功清除token和用户信息
user.value = null
token.value = ''
localStorage.removeItem('token')
callback&&callback();
return res
}
return res
@ -56,6 +62,7 @@ export const useAuthStore = defineStore('auth', () => {
login,
logout,
getUserInfo,
loginSuccess,
loading
}
})

View File

@ -0,0 +1,124 @@
/* Restore Element Plus ElMessage default-like styles and neutralize dark overrides
This file is intentionally loaded last to ensure message styles match Element's
appearance rather than project-specific dark-mode overrides. */
.el-message {
position: fixed !important;
top: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 9999 !important;
align-items: center;
display: flex;
gap: 8px;
box-sizing: border-box;
max-width: calc(100% - 32px);
padding: 11px 15px !important;
border-radius: 4px !important;
border-style: solid !important;
border-width: 1px !important;
background-color: #fff !important;
border-color: #ebeef5 !important;
color: #303133 !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
transition: all 0.3s ease !important;
opacity: 1 !important;
}
/* ElMessage transition animations */
.el-message-fade-enter-active,
.el-message-fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.el-message-fade-enter-from,
.el-message-fade-leave-to {
opacity: 0 !important;
transform: translateX(-50%) translateY(-100%) !important;
}
.el-message-fade-leave-from {
opacity: 1 !important;
transform: translateX(-50%) translateY(0%) !important;
}
/* ElMessage types */
.el-message--success {
background-color: #f0f9ff !important;
border-color: #b3e5fc !important;
color: #1e88e5 !important;
}
.el-message--warning {
background-color: #fff8e1 !important;
border-color: #ffcc02 !important;
color: #f57c00 !important;
}
.el-message--error {
background-color: #ffebee !important;
border-color: #ffcdd2 !important;
color: #d32f2f !important;
}
.el-message--info {
background-color: #f0f9ff !important;
border-color: #b3e5fc !important;
color: #1e88e5 !important;
}
.el-message p,
.el-message__content {
margin: 0;
color: inherit !important;
overflow-wrap: break-word;
font-size: 14px !important;
line-height: 1.5 !important;
}
.el-message__icon {
color: inherit !important;
font-size: 16px !important;
}
.el-message__closeBtn {
color: var(--el-message-close-icon-color, inherit) !important;
}
/* Neutralize overly-specific dark-mode overrides that use html.dark and !important
by re-declaring rules without relying on html.dark. Loaded last, these will win. */
html.dark .el-message {
background-color: #1f2937 !important;
border-color: #374151 !important;
color: #f9fafb !important;
}
html.dark .el-message--success {
background-color: #065f46 !important;
border-color: #10b981 !important;
color: #d1fae5 !important;
}
html.dark .el-message--warning {
background-color: #78350f !important;
border-color: #f59e0b !important;
color: #fef3c7 !important;
}
html.dark .el-message--error {
background-color: #7f1d1d !important;
border-color: #ef4444 !important;
color: #fecaca !important;
}
html.dark .el-message--info {
background-color: #1e3a8a !important;
border-color: #3b82f6 !important;
color: #dbeafe !important;
}
html.dark .el-message__icon,
html.dark .el-message__content,
html.dark .el-message__closeBtn {
color: inherit !important;
}

View File

@ -239,79 +239,6 @@ html.dark .el-switch__input:focus + .el-switch__core {
outline: none !important;
}
html.dark .el-message__icon {
color: #f3f4f6 !important;
}
html.dark .el-message__content {
color: #f3f4f6 !important;
}
html.dark .el-message__closeBtn {
color: #9ca3af !important;
}
html.dark .el-message__closeBtn:hover {
color: #f3f4f6 !important;
}
/* Success message in dark theme */
html.dark .el-message--success {
background-color: rgba(16, 185, 129, 0.1) !important;
border-color: #10b981 !important;
}
html.dark .el-message--success .el-message__icon {
color: #10b981 !important;
}
html.dark .el-message--success .el-message__content {
color: #a7f3d0 !important;
}
/* Warning message in dark theme */
html.dark .el-message--warning {
background-color: rgba(245, 158, 11, 0.1) !important;
border-color: #f59e0b !important;
}
html.dark .el-message--warning .el-message__icon {
color: #f59e0b !important;
}
html.dark .el-message--warning .el-message__content {
color: #fcd34d !important;
}
/* Error message in dark theme */
html.dark .el-message--error {
background-color: rgba(239, 68, 68, 0.1) !important;
border-color: #ef4444 !important;
}
html.dark .el-message--error .el-message__icon {
color: #ef4444 !important;
}
html.dark .el-message--error .el-message__content {
color: #fca5a5 !important;
}
/* Info message in dark theme */
html.dark .el-message--info {
background-color: rgba(107, 114, 128, 0.1) !important;
border-color: #6b7280 !important;
}
html.dark .el-message--info .el-message__icon {
color: #6b7280 !important;
}
html.dark .el-message--info .el-message__content {
color: #d1d5db !important;
}
/* Element Plus Notification component dark theme styles */
html.dark .el-notification {
background-color: #1f2937 !important;
@ -336,28 +263,6 @@ html.dark .el-notification__closeBtn:hover {
color: #f3f4f6 !important;
}
/* Element Plus MessageBox component dark theme styles */
html.dark .el-message-box {
background-color: #1f2937 !important;
border-color: #374151 !important;
color: #f3f4f6 !important;
}
html.dark .el-message-box__title {
color: #f3f4f6 !important;
}
html.dark .el-message-box__content {
color: #d1d5db !important;
}
html.dark .el-message-box__headerbtn {
color: #9ca3af !important;
}
html.dark .el-message-box__headerbtn:hover {
color: #f3f4f6 !important;
}
/* Element Plus Tooltip component dark theme styles */
html.dark .el-tooltip__popper {

View File

@ -1,159 +0,0 @@
import axios from 'axios';
import URL from './api/index';
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // 使用环境变量配置基础URL
timeout: 300000, // 请求超时时间
// 不设置默认的Content-Type让axios根据数据类型自动设置
});
// 请求拦截器
// 重点在请求拦截器中实现token的自动注入
// 这确保了每次API请求都会携带用户的身份认证信息
service.interceptors.request.use(
config => {
// 从localStorage中获取token
const token = localStorage.getItem('token');
if (token) {
// 将token添加到请求头
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => {
// 请求错误处理
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
// 直接返回响应数据
const res = response.data;
// 可以根据项目需求添加自定义响应状态码处理
// 例如如果res.code !== 200则认为是错误
// if (res.code !== 200) {
// return Promise.reject(new Error(res.message || 'Error'));
// }
return res;
},
error => {
// 响应错误处理
let message = '网络请求失败';
if (error.response) {
// 服务器返回错误状态码
switch (error.response.status) {
case 401:
message = '未授权,请重新登录';
// 可以在这里添加跳转登录页面的逻辑
break;
case 403:
message = '拒绝访问';
break;
case 404:
message = '请求的资源不存在';
break;
case 500:
message = '服务器内部错误';
break;
default:
message = error.response.data?.message || `请求错误(${error.response.status})`;
}
} else if (error.request) {
// 请求已发送但没有收到响应
message = '网络连接失败,请检查网络';
}
console.error(message);
// 可以在这里添加全局错误提示如使用Element Plus的Message
// Message.error(message);
return Promise.reject(error);
}
);
// 封装请求方法
export const request = {
url:URL,
// GET请求
get(url, params = {}) {
return service({
url,
method: 'get',
params
});
},
// POST请求
post(url, data = {}, params = {}) {
return service({
url,
method: 'post',
data,
params
});
},
// PUT请求
put(url, data = {}, params = {}) {
return service({
url,
method: 'put',
data,
params
});
},
// DELETE请求
delete(url, params = {}) {
return service({
url,
method: 'delete',
params
});
},
common(plug,data={}){
const config = {
url:plug.url,
method:plug.method,
params:data
};
if (plug.method.toLowerCase() === 'get') {
config.params = data;
} else {
config.data = data;
}
return service(config);
},
// 上传文件
upload(url, file, onUploadProgress) {
const formData = new FormData();
formData.append('file', file);
return service({
url,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress
});
},
// 下载文件
download(url, params = {}) {
return service({
url,
method: 'get',
params,
responseType: 'blob'
});
}
};
export default service;

View File

@ -1,71 +1,32 @@
<template>
<div class="forgot-password-page">
<!-- 全屏背景 -->
<div class="forgot-password-background"></div>
<!-- 右上角控制组件 -->
<div class="top-right-controls">
<div class="controls-container">
<ThemeToggle
position="top-right"
:tooltip-text="t('forgotPassword.theme_toggle_tooltip')"
/>
<LanguageToggle
position="top-right"
:tooltip-text="t('forgotPassword.language_toggle_tooltip')"
/>
</div>
<!-- 背景装饰 -->
<div class="background-decoration">
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="gradient-orb orb-3"></div>
</div>
<!-- 主忘记密码卡片 -->
<div class="forgot-password-container">
<div class="forgot-password-card">
<!-- 返回登录链接 -->
<div class="back-to-login">
<button @click="goToLogin" class="back-link">
<el-icon class="back-icon"><ArrowLeft /></el-icon>
{{ t('forgotPassword.back_to_login') }}
</button>
</div>
<!-- 忘记密码标题 -->
<div class="forgot-password-header">
<div class="title-icon">
<el-icon class="reset-icon"><Key /></el-icon>
</div>
<h1 class="forgot-password-title">{{ t('forgotPassword.title') }}</h1>
<p class="forgot-password-subtitle">{{ t('forgotPassword.subtitle') }}</p>
</div>
<!-- 忘记密码表单 -->
<div class="forgot-password-form-section">
<ForgotPasswordForm
@success="handleResetSuccess"
@error="handleResetError"
:loading="authStore.loading"
/>
</div>
<!-- 已有账号提示 -->
<div class="login-prompt">
<span class="prompt-text">{{ t('forgotPassword.remember_password') }}</span>
<button @click="goToLogin" class="login-link">
{{ t('forgotPassword.login_now') }}
</button>
</div>
<!-- 注册账号提示 -->
<div class="register-prompt">
<span class="prompt-text">{{ t('forgotPassword.no_account') }}</span>
<button @click="goToRegister" class="register-link">
{{ t('forgotPassword.register_now') }}
</button>
</div>
<!-- 错误提示 -->
<div v-if="authStore.error" class="error-message">
<el-icon class="error-icon"><WarningFilled /></el-icon>
<span>{{ authStore.error }}</span>
<!-- 右上角控制组件 -->
<div class="top-controls">
<ThemeToggle />
<LanguageToggle />
</div>
<!-- 主卡片 -->
<div class="main-card">
<!-- 返回按钮 -->
<div class="back-section">
<router-link to="/login" class="back-button">
<el-icon><ArrowLeft /></el-icon>
<span>{{ $t('forgotPassword.back_to_login') }}</span>
</router-link>
</div>
<!-- 表单区域 -->
<div class="form-section">
<div class="form-content">
<ForgotPasswordForm @reset-success="handleResetSuccess" />
</div>
</div>
</div>
@ -73,11 +34,11 @@
</template>
<script setup>
import { onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { ArrowLeft, WarningFilled, Key } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
//
import ForgotPasswordForm from '@/components/auth/ForgotPasswordForm.vue'
@ -85,320 +46,170 @@ import ThemeToggle from '@/components/ui/ThemeToggle.vue'
import LanguageToggle from '@/components/ui/LanguageToggle.vue'
const router = useRouter()
const authStore = useAuthStore()
const { t } = useI18n()
const resetEmail = ref('')
//
const handleResetSuccess = (userData) => {
console.log('密码重置请求成功:', userData)
//
//
setTimeout(() => {
router.push('/login')
}, 2000)
const handleResetSuccess = (email) => {
resetEmail.value = email
ElMessage.success(t('forgotPassword.reset_success_title'))
}
//
const handleResetError = (error) => {
console.error('密码重置失败:', error)
}
//
const goToLogin = () => {
router.push('/login')
}
//
const goToRegister = () => {
router.push('/register')
}
//
onMounted(() => {
//
if (authStore.isAuthenticated) {
if (authStore.isCreator) {
router.push('/creator')
} else if (authStore.isAdmin) {
router.push('/admin')
} else {
router.push('/dashboard')
}
}
// useAuthStore
// 使 useAuthStore
})
</script>
<style scoped>
/* 忘记密码页面基础样式 */
/* 忘记密码页面容器 */
.forgot-password-page {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
position: relative;
overflow: hidden;
}
/* 全屏背景渐变 - 使用与登录页相同的背景 */
.forgot-password-background {
/* 背景装饰 */
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
#6B46C1 0%,
#8B5CF6 25%,
#A78BFA 50%,
#DDD6FE 75%,
#F3F4F6 100%);
background-size: 400% 400%;
animation: gradientShift 8s ease infinite;
width: 100%;
height: 100%;
z-index: 1;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.3;
animation: float 6s ease-in-out infinite;
}
/* 主忘记密码容器 */
.forgot-password-container {
position: relative;
.orb-1 {
width: 300px;
height: 300px;
background: linear-gradient(135deg, #7c3aed, #6b46c1);
top: 10%;
left: 10%;
animation-delay: 0s;
}
.orb-2 {
width: 200px;
height: 200px;
background: linear-gradient(135deg, #a78bfa, #8b5cf6);
top: 60%;
right: 15%;
animation-delay: 2s;
}
.orb-3 {
width: 150px;
height: 150px;
background: linear-gradient(135deg, #c4b5fd, #a78bfa);
bottom: 20%;
left: 30%;
animation-delay: 4s;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
/* 右上角控制组件 */
.top-controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
display: flex;
gap: 12px;
}
/* 主卡片 */
.main-card {
position: relative;
z-index: 2;
width: 100%;
max-width: 440px;
padding: 20px;
}
/* 忘记密码卡片 */
.forgot-password-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
backdrop-filter: blur(10px);
border-radius: 24px;
padding: 48px 40px;
padding: 0;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.2);
transform: translateY(0);
transition: all 0.3s ease;
animation: slideInUp 0.6s ease-out;
position: relative;
0 10px 10px -5px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: fadeInUp 0.6s ease-out;
overflow: hidden;
}
/* 卡片微妙的背景动画 */
.forgot-password-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(139, 92, 246, 0.03) 0%,
transparent 50%,
rgba(167, 139, 250, 0.03) 100%);
pointer-events: none;
z-index: -1;
/* 返回按钮 */
.back-section {
padding: 20px 24px 0;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
.back-button {
display: inline-flex;
align-items: center;
gap: 8px;
background: none;
border: none;
color: #6b7280;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
padding: 8px 12px;
border-radius: 8px;
text-decoration: none;
}
.forgot-password-card:hover {
transform: translateY(-2px);
box-shadow:
0 25px 30px -5px rgba(0, 0, 0, 0.15),
0 15px 15px -5px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(139, 92, 246, 0.15);
.back-button:hover {
background: rgba(107, 70, 193, 0.1);
color: #6b46c1;
}
/* 返回登录链接 */
.back-to-login {
/* 表单区域 */
.form-section {
padding: 0 24px 24px;
}
.form-header {
text-align: center;
margin-bottom: 24px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
background: none;
border: none;
color: #6B46C1;
font-size: 14px;
font-weight: 500;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s ease;
}
.back-link:hover {
background: rgba(107, 70, 193, 0.1);
color: #5B21B6;
}
.back-icon {
font-size: 14px;
}
/* 忘记密码标题 */
.forgot-password-header {
text-align: center;
margin-bottom: 32px;
}
.title-icon {
display: flex;
justify-content: center;
align-items: center;
width: 64px;
height: 64px;
background: linear-gradient(135deg, #6B46C1, #8B5CF6);
border-radius: 16px;
margin: 0 auto 16px;
box-shadow: 0 8px 16px rgba(107, 70, 193, 0.3);
}
.reset-icon {
font-size: 28px;
color: white;
}
.forgot-password-title {
font-size: 28px;
.form-title {
font-size: 24px;
font-weight: 700;
color: #1F2937;
color: #1f2937;
margin: 0 0 8px 0;
background: linear-gradient(135deg, #6B46C1, #8B5CF6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.forgot-password-subtitle {
font-size: 16px;
color: #6B7280;
.form-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.5;
}
/* 忘记密码表单区域 */
.forgot-password-form-section {
margin-bottom: 32px;
}
/* 登录提示 */
.login-prompt {
text-align: center;
padding-top: 16px;
border-top: 1px solid rgba(139, 92, 246, 0.1);
margin-bottom: 8px;
}
.prompt-text {
color: #6B7280;
font-size: 14px;
margin-right: 8px;
}
.login-link {
background: none;
border: none;
color: #6B46C1;
font-size: 14px;
font-weight: 600;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
}
.login-link:hover {
background: rgba(107, 70, 193, 0.1);
color: #5B21B6;
}
/* 注册账号提示 */
.register-prompt {
text-align: center;
padding-top: 8px;
}
.register-link {
background: none;
border: none;
color: #6B46C1;
font-size: 14px;
font-weight: 600;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
}
.register-link:hover {
background: rgba(107, 70, 193, 0.1);
color: #5B21B6;
}
/* 错误消息 */
.error-message {
display: flex;
align-items: center;
gap: 8px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 12px;
padding: 12px 16px;
color: #EF4444;
font-size: 14px;
margin-top: 20px;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.error-icon {
flex-shrink: 0;
}
/* 右上角控制组件样式 */
.top-right-controls {
position: fixed;
top: 24px;
right: 24px;
z-index: 1000;
display: flex;
justify-content: flex-end;
align-items: center;
box-sizing: border-box;
}
/* 容器包装控制组件 */
.controls-container {
display: flex;
gap: 12px;
margin-left: auto;
.form-content {
margin-bottom: 0;
}
/* 响应式设计断点 */

View File

@ -130,32 +130,15 @@ const handleLoginSuccess = (userData) => {
}
}
// Google
const handleGoogleLoginSuccess = (userData) => {
console.log('Google 登录成功:', userData)
handleLoginSuccess(userData)
}
//
const handleEmailLoginSuccess = (userData) => {
console.log('邮箱登录成功:', userData)
handleLoginSuccess(userData)
}
//
const goToForgotPassword = () => {
router.push('/forgot-password')
}
//
const goToRegister = () => {
router.push('/register')
}
//
onMounted(() => {
})

View File

@ -1,4 +1,4 @@
import request from '@/utils/request.js';
import { requestUtils,clientApi } from '@deotaland/utils';
import { useRouter } from 'vue-router'
import {ElMessage} from 'element-plus';
import { useAuthStore } from '@/stores/auth.js';
@ -7,7 +7,69 @@ export default class Login {
this.router = useRouter();
this.authStore = useAuthStore();
}
async login(data) {
this.authStore.login(data);
async login(data) {
this.authStore.login(data,()=>{
this.router.push({ name: 'home' })
// this.refreshGoogleRefreshToken()
});
}
//发送邮箱验证码
sendEmailCode(item,callback){
requestUtils.common(clientApi.default.SEND_EMAIL_CODE,{
email:item.email,
purpose:item.purpose||'register' //forgot-password
}).then(res=>{
if(res.code === 200){
ElMessage.success('验证码发送成功');
callback&&callback();
}
})
}
//确认注册功能
confirmRegister(data,callback){
let params = {
"email": data.email,
"emailCode": data.emailCode,
"password": data.password
}
requestUtils.common(clientApi.default.REGISTER,params).then(res=>{
if(res.code === 200){
let data = res.data;
this.authStore.loginSuccess(data,()=>{
this.router.push({ name: 'home' })
})
ElMessage.success('注册成功');
callback&&callback();
}
})
}
//刷新googleRefreshToken
refreshGoogleRefreshToken(callback){
requestUtils.common(clientApi.default.REFRESH_TOKEN).then(res=>{
if(res.code === 200){
ElMessage.success('刷新成功');
callback&&callback();
}
})
}
//登出
logout(){
this.authStore.logout(()=>{
this.router.replace({ name: 'login' })
})
}
//确认修改密码
confirmForgotPassword(data,callback){
let params = {
"email": data.email,
"emailCode": data.emailCode,
"newPassword": data.password
}
requestUtils.common(clientApi.default.FORGOT_PASSWORD,params).then(res=>{
if(res.code === 200){
ElMessage.success('密码修改成功');
callback&&callback();
}
})
}
}

View File

@ -9,15 +9,36 @@
<div class="logo-glow"></div>
</div>
<h1 class="welcome-title">
<span class="greeting">{{ t('home.welcome.title', { name: userName || t('home.welcome.defaultName') }) }}</span>
<span class="greeting" v-if="isLoggedIn">{{ t('home.welcome.title', { name: userName }) }}</span>
<span class="greeting" v-else>{{ t('home.welcome.title', { name: t('home.welcome.defaultName') }) }}</span>
</h1>
<p class="welcome-subtitle">
{{ t('home.welcome.greetingMessage') }}
</p>
<div class="welcome-actions">
<el-button type="primary" size="large" class="action-btn primary-btn create-btn-large" @click="navigateToFeature({ path: '/create-project' })">
<el-button
v-if="isLoggedIn"
type="primary"
size="large"
class="action-btn primary-btn create-btn-large"
@click="navigateToFeature({ path: '/create-project' })">
{{ t('home.welcome.startCreating') }}
</el-button>
<div v-else class="guest-actions">
<el-button
type="primary"
size="large"
class="action-btn primary-btn create-btn-large"
@click="navigateToFeature({ path: '/login' })">
{{ t('home.welcome.loginToStart') }}
</el-button>
<el-button
size="large"
class="action-btn secondary-btn"
@click="navigateToFeature({ path: '/register' })">
{{ t('home.welcome.register') }}
</el-button>
</div>
</div>
</div>
<div class="welcome-right">
@ -44,9 +65,13 @@
</div>
</div>
<div class="welcome-avatar">
<el-avatar :size="100" :src="userAvatar">
<el-avatar :size="100" :src="userAvatar" v-if="isLoggedIn">
<el-icon size="50"><User /></el-icon>
</el-avatar>
<div v-else class="guest-avatar" @click="navigateToFeature({ path: '/login' })">
<el-icon size="50"><User /></el-icon>
<div class="guest-login-tip">{{ t('home.welcome.clickToLogin') }}</div>
</div>
</div>
</div>
</div>
@ -79,8 +104,6 @@
</div>
</div>
</section>
</div>
</template>
@ -103,7 +126,7 @@ import {
ArrowDown,
Minus,
VideoPlay,
ChatDotRound ,
ChatDotRound,
Tickets,
Setting,
Picture,
@ -182,13 +205,11 @@ export default {
trendValue: '0%'
}
])
//
const currentUser = computed(() => authStore.user)
const userName = computed(() => currentUser.value?.name || '')
const userAvatar = computed(() => currentUser.value?.avatar || '')
const userName = computed(() => currentUser.value?.nickname || '')
const userAvatar = computed(() => currentUser.value?.avatarUrl || '')
const isLoggedIn = computed(() => authStore.token)
//
const navigateToFeature = (feature) => {
@ -223,6 +244,7 @@ export default {
statsData,
userName,
userAvatar,
isLoggedIn,
navigateToFeature
}
}
@ -597,6 +619,44 @@ export default {
animation: fadeInUp 0.8s ease-out 1.0s both;
}
.guest-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.3);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
color: rgba(255, 255, 255, 0.8);
}
.guest-avatar:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.05);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.guest-login-tip {
font-size: 10px;
color: rgba(255, 255, 255, 0.8);
margin-top: 4px;
text-align: center;
line-height: 1.2;
}
.guest-actions {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
/* 响应式设计 */
@media (max-width: 768px) {
.welcome-content {

View File

@ -52,7 +52,15 @@ export default defineConfig({
},
server: {
port: 3000,
host: true
host: true,
// 配置代理解决CORS问题
proxy: {
'/api': {
target: 'https://api.deotaland.ai',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
preview: {
port: 3000,

View File

@ -0,0 +1,6 @@
const login = {
LOGIN:{url:'/admin/login',method:'POST'},// 登录
CAPTCHA_CODE:{url:'/captcha/code',method:'GET'},// 后台验证码
LOGOUT:{url:'/admin/logout',method:'POST'},// 管理端登出
}
export default login;

View File

@ -0,0 +1,6 @@
import login from './login.js';
import meshy from './meshy.js';
export default {
...meshy,
...login,
};

View File

@ -4,6 +4,7 @@ const login = {
REGISTER:{url:'/user/register',method:'POST'},// 注册
SEND_EMAIL_CODE:{url:'/user/send-email-code',method:'POST'},// 发送邮箱验证码
OAUTH_GOOGLE:{url:'/user/oauth/google',method:'POST'},// google弹窗授权
OAUTH_GOOGLE_CODE:{url:'/captcha/code',method:'GET'},// 后台验证码
FORGOT_PASSWORD:{url:'/user/forgot-password',method:'POST'},// 修改密码
REFRESH_TOKEN:{url:'/user/oauth/google/refresh',method:'POST'},// googleRefreshToken刷新
}
export default login;

View File

@ -0,0 +1,6 @@
const login = {
UPLOAD:{url:'/api-core/front/data/upload',method:'POST'},// 文件上传
IMAGE_TO_3D:{url:'/api-core/front/mesh/image-to-3d',method:'POST'},// 图片转3D模型任务提交
FIND_TASK_ID:{url:'/api-core/front/mesh/image-to-3d/TASKID',method:'GET'},// 获取3D模型任务查询
}
export default login;

View File

@ -11,7 +11,10 @@ import * as dateUtils from './utils/date.js'
import * as fileUtils from './utils/file.js'
import * as validateUtils from './utils/validate.js'
import * as formatUtils from './utils/format.js'
import { request as requestUtils } from './utils/request.js'
import * as adminApi from './api/frontend/index.js';
import * as clientApi from './api/frontend/index.js';
import { MeshyServer } from './servers/meshyserver.js';
// 合并所有工具函数
const deotalandUtils = {
string: stringUtils,
@ -21,7 +24,10 @@ const deotalandUtils = {
file: fileUtils,
validate: validateUtils,
format: formatUtils,
request: requestUtils,
adminApi,
clientApi,
MeshyServer,
// 全局常用方法
debounce: stringUtils.debounce || createDebounce(),
throttle: stringUtils.throttle || createThrottle(),
@ -43,6 +49,10 @@ export {
fileUtils,
validateUtils,
formatUtils,
requestUtils,
adminApi,
clientApi,
MeshyServer,
}
/**

View File

@ -0,0 +1,46 @@
import { requestUtils,clientApi } from "../index";
let urlRule = 'https://api.deotaland.aiIMGURL'
export class FileServer {
constructor() {
}
//上传文件
async uploadFile(url) {
return new Promise(async (resolve, reject) => {
const file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象
const formData = new FormData();
// 从URL中提取文件名如果没有则使用默认文件名
const fileName = url.split('/').pop() || 'uploaded_file';
formData.append('file', file, fileName);
try {
const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData);
if(response.code==0){
let data = response.data;
if(data.url){
resolve(urlRule.replace('IMGURL',data.url));
}
}
} catch (error) {
reject(error);
console.error('上传文件失败:', error);
throw error;
}
})
}
//文件文件或者base64文件转为blob对象
fileToBlob(fileOrBase64) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', fileOrBase64);
xhr.responseType = 'blob';
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response);
} else {
reject(new Error('文件转换失败'));
}
};
xhr.onerror = reject;
xhr.send();
});
}
}

View File

@ -0,0 +1,66 @@
import { requestUtils,clientApi } from "../index";
import { FileServer } from './fileserver.js';
export class MeshyServer extends FileServer {
constructor() {
super();
}
//提交模型任务返回id
async createModelTask(item,callback) {
try {
let params = {
project_id: item.project_id,
"payload": {
image_url:'',
ai_model: 'latest',
enable_pbr: true,
should_remesh: true,
should_texture: true,
save_pre_remeshed_model: true
}
}
let imgurl = await this.uploadFile(item.image_url);
// let imgurl = 'https://api.deotaland.ai/upload/aabf8b4a8df447fa8c3e3f7978c523cc.png';
params.payload.image_url = imgurl;
const response = await requestUtils.common(clientApi.default.IMAGE_TO_3D, params);
// const response = {
// "code": 0,
// "message": "",
// "success": true,
// "data": {
// "result": "019aba1c-86c4-7572-a816-1986f9ef5d0e"
// }
// };
if(response.code==0){
callback&&callback(response?.data?.result);
}
} catch (error) {
console.error('创建模型任务失败:', error);
throw error;
}
}
//查询任务状态
async getModelTaskStatus(result,callback,errorCallback,progressCallback){
const requestUrl = clientApi.default.FIND_TASK_ID.replace('TASKID', result);
let response = await requestUtils.common(requestUrl, {});
if(response.code==0){
let data = response?.data
switch (data.status) {
case "SUCCEEDED":
let modelurl = data.model_url.replace("https://assets.meshy.ai", "https://api.deotaland.ai/model");
callback&&callback(modelurl);
break;
case "FAILED":
errorCallback&&errorCallback();
break;
case "CANCELED":
errorCallback&&errorCallback();
break;
default:
// 等待三秒
await new Promise(resolve => setTimeout(resolve, 3000));
progressCallback&&progressCallback(data.progress);
break;
}
}
}
}

View File

@ -0,0 +1,243 @@
import axios from 'axios';
// 获取环境变量中的基础URL
const getEnvBaseURL = () => {
// 浏览器环境
if (typeof window !== 'undefined') {
// Vite 环境变量
if (import.meta?.env?.VITE_BASE_URL) {
return import.meta.env.VITE_BASE_URL;
}
// 其他环境变量
return window.VITE_BASE_URL || window.BASE_URL || '';
}
// Node.js 环境
if (typeof process !== 'undefined') {
return process.env.VITE_BASE_URL || process.env.BASE_URL || '';
}
return '';
};
// 创建axios实例
const service = axios.create({
timeout: 300000, // 请求超时时间 5分钟
baseURL: getEnvBaseURL(), // 使用环境变量中的基础URL
// 不设置默认的Content-Type让axios根据数据类型自动设置
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 从localStorage中获取token如果存在
const token = localStorage.getItem('token');
if (token) {
// 将token添加到请求头
config.headers['Authorization'] = `${token}`;
config.headers['token'] = `${token}`;
}
return config;
},
error => {
// 请求错误处理
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
// 直接返回响应数据
const res = response.data;
return res;
},
error => {
// 响应错误处理
let message = '网络请求失败';
if (error.response) {
// 服务器返回错误状态码
switch (error.response.status) {
case 401:
message = '未授权,请重新登录';
break;
case 403:
message = '拒绝访问';
break;
case 404:
message = '请求的资源不存在';
break;
case 500:
message = '服务器内部错误';
break;
default:
message = error.response.data?.message || `请求错误(${error.response.status})`;
}
} else if (error.request) {
// 请求已发送但没有收到响应
message = '网络连接失败,请检查网络';
}
console.error(message);
return Promise.reject(error);
}
);
// 封装请求方法
export const request = {
/**
* 设置基础URL
* @param {string} baseURL - 基础URL
*/
setBaseURL(baseURL) {
service.defaults.baseURL = baseURL;
},
/**
* GET请求
* @param {string} url - 请求URL
* @param {Object} params - URL参数
* @returns {Promise}
*/
get(url, params = {}) {
return service({
url,
method: 'get',
params
});
},
/**
* POST请求
* @param {string} url - 请求URL
* @param {Object} data - 请求体数据
* @param {Object} params - URL参数
* @returns {Promise}
*/
post(url, data = {}, params = {}) {
return service({
url,
method: 'post',
data,
params
});
},
/**
* PUT请求
* @param {string} url - 请求URL
* @param {Object} data - 请求体数据
* @param {Object} params - URL参数
* @returns {Promise}
*/
put(url, data = {}, params = {}) {
return service({
url,
method: 'put',
data,
params
});
},
/**
* DELETE请求
* @param {string} url - 请求URL
* @param {Object} params - URL参数
* @returns {Promise}
*/
delete(url, params = {}) {
return service({
url,
method: 'delete',
params
});
},
/**
* 通用请求方法
* @param {Object} config - 请求配置 {url, method}
* @param {Object} data - 请求数据
* @returns {Promise}
*/
common(config, data = {}) {
const requestConfig = {
url: config.url,
method: config.method,
};
if (config.method.toLowerCase() === 'get') {
requestConfig.params = data;
} else {
requestConfig.data = data;
}
return service(requestConfig);
},
/**
* 上传文件
* @param {string} url - 上传URL
* @param {FormData} formData - 表单数据
* @returns {Promise}
*/
upload(url, formData) {
return service({
url,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
});
},
/**
* 下载文件
* @param {string} url - 下载URL
* @param {Object} params - URL参数
* @returns {Promise}
*/
download(url, params = {}) {
return service({
url,
method: 'get',
params,
responseType: 'blob'
});
},
/**
* 设置请求头
* @param {string} key - 请求头键
* @param {string} value - 请求头值
*/
setHeader(key, value) {
service.defaults.headers.common[key] = value;
},
/**
* 移除请求头
* @param {string} key - 请求头键
*/
removeHeader(key) {
delete service.defaults.headers.common[key];
},
/**
* 获取axios实例用于高级用法
* @returns {AxiosInstance}
*/
getInstance() {
return service;
}
};
/**
* 重新初始化axios实例当环境变量改变时
*/
export const reinitialize = () => {
const newBaseURL = getEnvBaseURL();
service.defaults.baseURL = newBaseURL;
console.log(`Request service reinitialized with baseURL: ${newBaseURL}`);
};
export default service;

View File

@ -32,9 +32,15 @@ importers:
apps/FrontendDesigner:
dependencies:
'@deotaland/utils':
specifier: ^0.0.1
version: 0.0.1
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.24)
'@google/genai':
specifier: ^1.27.0
version: 1.27.0
'@types/three':
specifier: ^0.180.0
version: 0.180.0
@ -81,12 +87,9 @@ importers:
apps/frontend:
dependencies:
'@deotaland/ui':
specifier: workspace:*
version: link:../../packages/ui
'@deotaland/utils':
specifier: workspace:*
version: link:../../packages/utils
specifier: ^0.0.1
version: 0.0.1
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.24)
@ -151,6 +154,9 @@ importers:
'@iconify-json/feather':
specifier: ^1.2.1
version: 1.2.1
'@vitejs/plugin-vue':
specifier: ^6.0.1
version: 6.0.1(vite@7.2.2)(vue@3.5.24)
unplugin-auto-import:
specifier: ^20.2.0
version: 20.2.0
@ -160,6 +166,9 @@ importers:
unplugin-vue-components:
specifier: ^30.0.0
version: 30.0.0(vue@3.5.24)
vite:
specifier: ^7.2.2
version: 7.2.2
packages/ui:
dependencies:

13
vertex-ai-key.json Normal file
View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "balmy-virtue-478504-j9",
"private_key_id": "d125d99738b73801ab95346f569f69d4555cff2d",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCh6MrzXOMyW7Pv\nhLt/LCMLWPdTp5JmW7aUQ3+xIaIdGCzT4kpscYLDZhjRugxXtdll92xc7qVj+Pw7\nNsI+/m3IirGZaF8ZlbIVYEAwE4T93pGLMjPpdbS5wMoqsGM2rHetFrusdUR9qqTk\nk4uDEGlhiPX+6fRVaRPirr+n9JvxJtPVgmqKCHkyBzbDG6ICVluDkbqINOfP0v5i\nUQX5DLDsaNIR2TjLLkUgVTsvUhfclyJv953aUBq64mwPsrvtnJtcJULATlDSb/YE\noiA3q+Q8jZpaLkvTGpCeikJ7gRaUwVJlIZegLapLSfUXO6LHJi1050Oo2wnGEFX4\njkp07Z25AgMBAAECggEAQl4aUweQWeQdLeFKuiZtdwlQ2ImoCS0u+jdw8DrJKQPv\n3Cq2Nx2QbGg9ZDrPNGTmaFWzpaRtRz2Ypu0bUpcYiUvQ4QFXejVSelCp/wsBSM8i\n+dvqS5hkLIBKXpVPFeo8ZEcdRuQK3zhDvy570Y24pLJvo75i2V/pNtJK3Z33DjIl\nVoE0bA6HvmuFDKYlIN4175X4Ywcpwxokb6pEXZYKHDsz/UmGo5t7cPlUV1lBIf5T\nY5QcoOKJx/5QnQgMNJ/B7xiEOzNUMAhteztcabxaewac648za/xjG4HTPkfS/xS2\n6FHQv3zPUzoLKxvN2t5aJbhD4xX9RABS/olz80qRwQKBgQDOfedvPnORBa1eNKXi\nl/bJODnhdDqXYSxNyn9WLYt0LDJZbbIPfn+VAp7BGKazGzLDA3TLCgVh8nDizsAZ\npiszw0yWZizN9U/ZQePy87B2iW0tzuCfV+jd3Ns90IXDb/mvKbri4q399RQnkrAi\nd4aAVfW6rvK266EhBlczQkuwcwKBgQDIun9HtayA+p00klFKQBtaDaGRe6mv7yXH\nhdb8G8Id7g5efNbFdTg5+/iZVrsEcEhERAjYGj+kZ2TzkiH27r4wG7hRSoG6BshP\n9NJh+OHs8XondCooVYkSvRAAjNbfMwnn32CsV4S3UHq8PUYxknFG7Q/wF2SVsXFG\nVrpYVZAKIwKBgBS1wfmBTPv1ks7I/v47+Y9y6TM4ggvevh/LOHw/MyZirGYVv28Y\nY9lhGuUJAOcjyjKO7S7UAXgyZaoJzHCGHv0hEFRhSQsbGHgUyLT8Re2NmPqoLhUt\nLvjZhs+rU08nsuYjjE/nJkY7R1s0th+u1zmV5YBkvYklFtMGHMbSVl8LAoGANfqx\nL7+TXDwI+pI+ehEzScxQnqb6wu0046sCXVm5ogLaql44A3G6ZR11hQbl1BO9213Q\nYwzsAHItm7K4n4ckbhuGPZYjvLsGMzpLOT2MxANMLj/29lHKQtfE7eDyB6PaDhjs\nDmyarBFgcC6qKbqP69rkZlRkID1PkPLRud+IlLECgYEArMeCvPcKFV81JjDKB4dJ\nXu/6njARyZ/WR6qnCOc50FivwXEbADLbUs3xTkEbtjxeU6/1E8eyK8lWwcIgXkoL\ns65wZXOqub6CX0ylOMG3TlBy1S1ruwWml+jthAv57SNH2UygfWpQtWKH3pj+l83H\n2qVS4e241p320MjlARGd4Ek=\n-----END PRIVATE KEY-----\n",
"client_email": "imagen-api-caller@balmy-virtue-478504-j9.iam.gserviceaccount.com",
"client_id": "104391582695292662277",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/imagen-api-caller%40balmy-virtue-478504-j9.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}