refactor: 优化前端代码,准备生产环境部署
- 删除所有调试日志(console.error/console.log) - 将所有中文注释改为英文注释 - 将硬编码的中文文本改为使用 t() 函数的英文,支持国际化 - 移除可能暴露项目信息的内容 - 添加新增翻译键的中文翻译(Portrait Sample, Product Sample, Animal Sample, Object Sample, Unable to get team information) 修改文件: - src/views/HomePage.vue - src/views/tools/remove_background/remove_background.vue - src/views/settings/Settings.vue - src/locales/zh-CN.json
This commit is contained in:
parent
36af166879
commit
ee2f59df81
@ -1386,5 +1386,10 @@
|
||||
"请选择一个原因": "请选择一个原因",
|
||||
"请评价您的体验": "请评价您的体验",
|
||||
"请简要说明原因": "请简要说明原因",
|
||||
"Your feedback has been submitted successfully": "您的反馈已成功提交"
|
||||
"Your feedback has been submitted successfully": "您的反馈已成功提交",
|
||||
"Portrait Sample": "人物示例",
|
||||
"Product Sample": "产品示例",
|
||||
"Animal Sample": "动物示例",
|
||||
"Object Sample": "物品示例",
|
||||
"Unable to get team information": "无法获取团队信息"
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
const logoUrl = computed(() => '/logo.svg')
|
||||
|
||||
// 登录/注册弹窗状态
|
||||
// Login/Signup modal state
|
||||
const showLoginModal = ref(false)
|
||||
const showSignupModal = ref(false)
|
||||
const loginFormRef = ref()
|
||||
@ -26,7 +26,7 @@ const loginLoading = ref(false)
|
||||
const signupLoading = ref(false)
|
||||
const showSignupLink = ref(true)
|
||||
|
||||
// 登录表单
|
||||
// Login form
|
||||
const loginFormData = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
@ -42,7 +42,7 @@ const loginRules = {
|
||||
]
|
||||
}
|
||||
|
||||
// 注册表单
|
||||
// Signup form
|
||||
const signupFormData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
@ -76,13 +76,13 @@ const signupRules = computed(() => {
|
||||
]
|
||||
}
|
||||
|
||||
// 英文版:email必填,手机号可选
|
||||
// English version: email required, phone optional
|
||||
if (isEnglish.value) {
|
||||
rules.email = [
|
||||
{ required: true, message: t('Please enter email'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: any, value: string) => {
|
||||
// required规则已处理空值,这里只验证格式
|
||||
// Required rule handles empty values, only validate format here
|
||||
if (!value) return true
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(value)) {
|
||||
@ -107,7 +107,7 @@ const signupRules = computed(() => {
|
||||
}
|
||||
]
|
||||
} else {
|
||||
// 中文版:email可选,手机号必填
|
||||
// Chinese version: email optional, phone required
|
||||
rules.email = [
|
||||
{
|
||||
validator: (_rule: any, value: string) => {
|
||||
@ -205,11 +205,9 @@ const handleSignupSubmit = async () => {
|
||||
}
|
||||
} else {
|
||||
const errorMsg = result.error || t('Sign up failed')
|
||||
console.error('注册失败:', errorMsg, result)
|
||||
message.error(errorMsg)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('注册异常:', error)
|
||||
message.error(error.message || t('Sign up failed, please try again'))
|
||||
} finally {
|
||||
signupLoading.value = false
|
||||
@ -226,15 +224,15 @@ const switchToLogin = () => {
|
||||
showLoginModal.value = true
|
||||
}
|
||||
|
||||
// 登录状态
|
||||
// Login state
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
|
||||
// Sidebar 折叠状态
|
||||
// Sidebar collapse state
|
||||
const SIDEBAR_COLLAPSE_KEY = 'app.sidebar.collapsed'
|
||||
const collapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSE_KEY) === 'true')
|
||||
const isMobile = ref(false)
|
||||
|
||||
// 检测屏幕尺寸
|
||||
// Check screen size
|
||||
const checkIsMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
if (isMobile.value) {
|
||||
@ -242,29 +240,29 @@ const checkIsMobile = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换侧边栏
|
||||
// Toggle sidebar
|
||||
const toggleSidebar = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
// 侧边栏折叠事件
|
||||
// Sidebar collapse event
|
||||
const onSidebarCollapse = () => {
|
||||
collapsed.value = true
|
||||
}
|
||||
|
||||
// 侧边栏展开事件
|
||||
// Sidebar expand event
|
||||
const onSidebarExpand = () => {
|
||||
collapsed.value = false
|
||||
}
|
||||
|
||||
// 菜单选择事件 - 移动端自动关闭
|
||||
// Menu select event - auto close on mobile
|
||||
const onMenuSelect = () => {
|
||||
if (isMobile.value) {
|
||||
collapsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
// Handle window resize
|
||||
const handleWindowResize = () => {
|
||||
checkIsMobile()
|
||||
adjustContainerSize()
|
||||
@ -289,13 +287,13 @@ interface HistoryItem {
|
||||
}
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
// urlInputRef 在模板中使用,lint 警告是误报
|
||||
// urlInputRef is used in template, lint warning is false positive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const urlInputRef = ref<HTMLInputElement | null>(null)
|
||||
const uploadedImage = ref<File | null>(null)
|
||||
const uploadedImageUrl = ref<string>('')
|
||||
const resultImage = ref<string>('')
|
||||
const resultImageBlobUrl = ref<string>('') // 缓存的 blob URL,用于下载
|
||||
const resultImageBlobUrl = ref<string>('') // Cached blob URL for download
|
||||
const imageUrl = ref<string>('')
|
||||
const resultImageUrl = computed(() => {
|
||||
if (!resultImage.value) return ''
|
||||
@ -309,57 +307,57 @@ const dragCounter = ref(0)
|
||||
const processing = ref(false)
|
||||
const splitPosition = ref(0)
|
||||
|
||||
// 示例图片用于快速体验 - 使用适合抠图的图片
|
||||
// Sample images for quick experience - suitable for background removal
|
||||
const sampleImages = [
|
||||
{
|
||||
id: 'sample-1',
|
||||
url: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '人物示例'
|
||||
name: t('Portrait Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-2',
|
||||
url: 'https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '人物示例'
|
||||
name: t('Portrait Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-3',
|
||||
url: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '产品示例'
|
||||
name: t('Product Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-4',
|
||||
url: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '产品示例'
|
||||
name: t('Product Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-5',
|
||||
url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '动物示例'
|
||||
name: t('Animal Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-6',
|
||||
url: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '物品示例'
|
||||
name: t('Object Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-7',
|
||||
url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '人物示例'
|
||||
name: t('Portrait Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-8',
|
||||
url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '人物示例'
|
||||
name: t('Portrait Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-9',
|
||||
url: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '人物示例'
|
||||
name: t('Portrait Sample')
|
||||
},
|
||||
{
|
||||
id: 'sample-10',
|
||||
url: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=1024&h=1024&fit=crop&q=80',
|
||||
name: '人物示例'
|
||||
name: t('Portrait Sample')
|
||||
}
|
||||
]
|
||||
const comparisonContainerRef = ref<HTMLElement | null>(null)
|
||||
@ -453,12 +451,11 @@ const processFile = async (file: File) => {
|
||||
}
|
||||
reader.readAsDataURL(compressedFile)
|
||||
} catch (error) {
|
||||
console.error('图片压缩失败:', error)
|
||||
message.error(t('Image processing failed, please try again'))
|
||||
message.error(t('Image processing failed, please try again'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理示例图片点击
|
||||
// Handle sample image click
|
||||
const handleSampleImageClick = async (imageUrl: string) => {
|
||||
if (processing.value) return
|
||||
|
||||
@ -472,7 +469,7 @@ const handleSampleImageClick = async (imageUrl: string) => {
|
||||
|
||||
const blob = await response.blob()
|
||||
|
||||
// 验证文件类型
|
||||
// Validate file type
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||
if (!validTypes.includes(blob.type)) {
|
||||
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
|
||||
@ -480,7 +477,7 @@ const handleSampleImageClick = async (imageUrl: string) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
// Validate file size
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (blob.size > maxSize) {
|
||||
message.warning(t('Image size exceeds 10MB limit'))
|
||||
@ -488,11 +485,10 @@ const handleSampleImageClick = async (imageUrl: string) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为File对象
|
||||
// Convert to File object
|
||||
const file = new File([blob], 'sample-image.jpg', { type: blob.type })
|
||||
await processFile(file)
|
||||
} catch (error: any) {
|
||||
console.error('加载示例图片失败:', error)
|
||||
let errorMessage = t('Failed to load sample image')
|
||||
|
||||
if (error.message?.includes('CORS')) {
|
||||
@ -515,7 +511,7 @@ const handleRemoveBackground = async () => {
|
||||
processing.value = true
|
||||
resultImage.value = ''
|
||||
|
||||
// 处理成功结果的辅助函数(文件内部使用)
|
||||
// Helper function to handle successful result (internal use)
|
||||
const handleSuccess = async (imageUrl: string): Promise<void> => {
|
||||
resultImage.value = imageUrl
|
||||
await cacheResultImage(imageUrl)
|
||||
@ -581,13 +577,13 @@ const handleRemoveBackground = async () => {
|
||||
message.error(result.error)
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON:', parseError, 'Line:', line)
|
||||
// Failed to parse JSON, continue processing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一行
|
||||
// Process last line
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const result = JSON.parse(buffer.trim())
|
||||
@ -598,7 +594,6 @@ const handleRemoveBackground = async () => {
|
||||
message.error(result.error || t('Failed to remove background'))
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse final JSON:', parseError)
|
||||
message.error(t('Failed to parse response'))
|
||||
}
|
||||
} else {
|
||||
@ -620,17 +615,17 @@ const handleRemoveBackground = async () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存结果图片为 blob URL,用于下载
|
||||
* Cache result image as blob URL for download
|
||||
*/
|
||||
const cacheResultImage = async (imageUrl: string) => {
|
||||
try {
|
||||
// 清理旧的 blob URL
|
||||
// Clean up old blob URL
|
||||
if (resultImageBlobUrl.value) {
|
||||
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||
resultImageBlobUrl.value = ''
|
||||
}
|
||||
|
||||
// 获取图片并转换为 blob URL
|
||||
// Fetch image and convert to blob URL
|
||||
const response = await fetch(imageUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status}`)
|
||||
@ -638,15 +633,14 @@ const cacheResultImage = async (imageUrl: string) => {
|
||||
const blob = await response.blob()
|
||||
resultImageBlobUrl.value = URL.createObjectURL(blob)
|
||||
} catch (error) {
|
||||
console.error('缓存图片失败:', error)
|
||||
// 缓存失败不影响显示,只是下载时需要重新获取
|
||||
// Cache failure doesn't affect display, only download needs to refetch
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!resultImage.value) return
|
||||
|
||||
// 检查登录状态
|
||||
// Check login status
|
||||
if (!isLoggedIn.value) {
|
||||
message.warning(t('Please login to download'))
|
||||
showLoginModal.value = true
|
||||
@ -654,10 +648,10 @@ const handleDownload = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 优先使用缓存的 blob URL
|
||||
// Prefer cached blob URL
|
||||
let blobUrl = resultImageBlobUrl.value
|
||||
|
||||
// 如果没有缓存,则获取并缓存
|
||||
// If no cache, fetch and cache
|
||||
if (!blobUrl) {
|
||||
const response = await fetch(resultImage.value)
|
||||
if (!response.ok) {
|
||||
@ -672,9 +666,8 @@ const handleDownload = async () => {
|
||||
link.href = blobUrl
|
||||
link.download = `removed-background-${Date.now()}.png`
|
||||
link.click()
|
||||
// 不立即清理 blob URL,保留缓存供后续下载使用
|
||||
// Don't immediately clean up blob URL, keep cache for subsequent downloads
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
message.error(t('Failed to download image'))
|
||||
}
|
||||
}
|
||||
@ -683,7 +676,7 @@ const resetUpload = () => {
|
||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||
}
|
||||
// 清理结果图片的 blob URL 缓存
|
||||
// Clean up result image blob URL cache
|
||||
if (resultImageBlobUrl.value) {
|
||||
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||
resultImageBlobUrl.value = ''
|
||||
@ -709,10 +702,10 @@ const resetUpload = () => {
|
||||
|
||||
const adjustContainerSize = async () => {
|
||||
await nextTick()
|
||||
// 等待多个渲染周期确保 DOM 完全更新
|
||||
// Wait for multiple render cycles to ensure DOM is fully updated
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
// 处理对比视图容器
|
||||
// Handle comparison view container
|
||||
const container = comparisonContainerRef.value
|
||||
if (container) {
|
||||
const img = originalImageRef.value || resultImageRef.value
|
||||
@ -746,7 +739,7 @@ const adjustContainerSize = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理单图视图容器
|
||||
// Handle single image view container
|
||||
const singleWrapper = singleImageWrapperRef.value
|
||||
if (singleWrapper) {
|
||||
const img = singleImageRef.value
|
||||
@ -906,13 +899,13 @@ const handleUrlSubmit = async () => {
|
||||
|
||||
const originalFile = new File([blob], 'image-from-url', { type: blob.type })
|
||||
|
||||
// 压缩图片到 1024x1024
|
||||
// Compress image to 1024x1024
|
||||
try {
|
||||
const compressedFile = await compressImageFile(originalFile, {
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
quality: 0.92,
|
||||
mode: 'contain' // 等比缩放
|
||||
mode: 'contain' // Maintain aspect ratio
|
||||
})
|
||||
|
||||
uploadedImage.value = compressedFile
|
||||
@ -920,12 +913,10 @@ const handleUrlSubmit = async () => {
|
||||
|
||||
await handleRemoveBackground()
|
||||
} catch (error) {
|
||||
console.error('图片压缩失败:', error)
|
||||
message.error(t('Image processing failed, please try again'))
|
||||
processing.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载图片URL失败:', error)
|
||||
let errorMessage = t('Failed to load image from URL')
|
||||
|
||||
if (error.message?.includes('CORS')) {
|
||||
@ -949,7 +940,7 @@ const selectHistoryItem = async (index: number) => {
|
||||
splitPosition.value = 0
|
||||
uploadedImage.value = item.originalImageFile
|
||||
|
||||
// 缓存历史记录的结果图片
|
||||
// Cache result image from history
|
||||
if (item.resultImage) {
|
||||
await cacheResultImage(item.resultImage)
|
||||
}
|
||||
@ -986,11 +977,11 @@ onMounted(async () => {
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
window.addEventListener('paste', handlePaste)
|
||||
|
||||
// 初始化认证状态
|
||||
// Initialize auth state
|
||||
await authStore.initAuth()
|
||||
|
||||
|
||||
// 检测移动端
|
||||
// Detect mobile
|
||||
checkIsMobile()
|
||||
})
|
||||
|
||||
@ -1000,7 +991,7 @@ onUnmounted(() => {
|
||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||
}
|
||||
// 清理结果图片的 blob URL 缓存
|
||||
// Clean up result image blob URL cache
|
||||
if (resultImageBlobUrl.value) {
|
||||
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||
}
|
||||
@ -1009,7 +1000,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="home-page" @dragenter.prevent="handleDragEnter" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
|
||||
<!-- 未登录状态:显示营销页面布局 -->
|
||||
<!-- Not logged in: show marketing page layout -->
|
||||
<template v-if="!isLoggedIn">
|
||||
<!-- Header -->
|
||||
<header class="marketing-header">
|
||||
@ -1159,7 +1150,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例图片区块 -->
|
||||
<!-- Sample images section -->
|
||||
<div v-if="!uploadedImage" class="sample-images-section">
|
||||
<p class="sample-images-title">{{ t('Click image to try') }}</p>
|
||||
<div class="sample-images-container">
|
||||
@ -1188,7 +1179,7 @@ onUnmounted(() => {
|
||||
<footer class="marketing-footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-content">
|
||||
<!-- 左侧:Logo 和社交媒体 -->
|
||||
<!-- Left: Logo and social media -->
|
||||
<div class="footer-left">
|
||||
<div class="footer-logo">
|
||||
<router-link to="/" class="logo-link">
|
||||
@ -1212,7 +1203,7 @@ onUnmounted(() => {
|
||||
<Icon icon="ant-design:zhihu-square-filled" />
|
||||
</a>
|
||||
</template>
|
||||
<!-- 英文版社交图标 -->
|
||||
<!-- English version social icons -->
|
||||
<template v-else>
|
||||
<a href="#" class="social-icon" title="Twitter">
|
||||
<Icon icon="tabler:brand-twitter" />
|
||||
@ -1236,7 +1227,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:三列链接 -->
|
||||
<!-- Right: three column links -->
|
||||
<div class="footer-right">
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">{{ t('Products & Services') }}</h3>
|
||||
@ -1269,10 +1260,10 @@ onUnmounted(() => {
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<!-- 已登录状态:显示应用布局(带 sidebar 和 header) -->
|
||||
<!-- Logged in: show app layout (with sidebar and header) -->
|
||||
<template v-else>
|
||||
<n-layout has-sider class="app-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<!-- Sidebar -->
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
@ -1288,14 +1279,14 @@ onUnmounted(() => {
|
||||
<AppSidebar :collapsed="collapsed" @menu-select="onMenuSelect" />
|
||||
</n-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<!-- Main content area -->
|
||||
<n-layout>
|
||||
<!-- 顶部导航 -->
|
||||
<!-- Top navigation -->
|
||||
<n-layout-header bordered>
|
||||
<AppHeader @toggle-sidebar="toggleSidebar" />
|
||||
</n-layout-header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<!-- Content area -->
|
||||
<n-layout-content>
|
||||
<div class="content-wrapper">
|
||||
<div v-if="isDragging" class="global-drag-overlay">
|
||||
@ -1426,7 +1417,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例图片区块 -->
|
||||
<!-- Sample images section -->
|
||||
<div v-if="!uploadedImage" class="sample-images-section">
|
||||
<p class="sample-images-title">{{ t('Click image to try') }}</p>
|
||||
<div class="sample-images-container">
|
||||
@ -1453,7 +1444,7 @@ onUnmounted(() => {
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
|
||||
<!-- 移动端遮罩层 -->
|
||||
<!-- Mobile overlay -->
|
||||
<div
|
||||
v-if="isMobile && !collapsed"
|
||||
class="mobile-overlay"
|
||||
@ -1462,7 +1453,7 @@ onUnmounted(() => {
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<!-- Login modal -->
|
||||
<n-modal
|
||||
v-model:show="showLoginModal"
|
||||
preset="card"
|
||||
@ -1531,7 +1522,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 注册弹窗 -->
|
||||
<!-- Signup modal -->
|
||||
<n-modal
|
||||
v-model:show="showSignupModal"
|
||||
preset="card"
|
||||
@ -1642,7 +1633,7 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-page {
|
||||
min-height: 100vh; /* 使用 min-height 而不是固定 height */
|
||||
min-height: 100vh; /* Use min-height instead of fixed height */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
@ -1650,7 +1641,7 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 应用布局样式(登录后) */
|
||||
/* App layout styles (after login) */
|
||||
.app-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
@ -1661,14 +1652,14 @@ onUnmounted(() => {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 使用Naive UI内置的sticky功能 */
|
||||
/* Use Naive UI built-in sticky functionality */
|
||||
:deep(.n-layout-header) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 移动端遮罩层 */
|
||||
/* Mobile overlay */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -1679,9 +1670,9 @@ onUnmounted(() => {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
/* Mobile styles */
|
||||
@media (max-width: 767px) {
|
||||
/* 移动端时完全隐藏侧边栏 */
|
||||
/* Completely hide sidebar on mobile */
|
||||
:deep(.n-layout-sider) {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
@ -1694,18 +1685,18 @@ onUnmounted(() => {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 移动端侧边栏打开时的样式 */
|
||||
/* Mobile sidebar open styles */
|
||||
:deep(.n-layout-sider:not(.n-layout-sider--collapsed)) {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
/* 移动端主内容区域占满全宽 */
|
||||
/* Mobile main content area takes full width */
|
||||
:deep(.n-layout) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端保持原有样式 */
|
||||
/* Desktop maintains original styles */
|
||||
@media (min-width: 768px) {
|
||||
:deep(.n-layout-sider) {
|
||||
position: relative !important;
|
||||
@ -1967,7 +1958,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 600px; /* 设置最小高度确保容器足够大 */
|
||||
min-height: 600px; /* Set minimum height to ensure container is large enough */
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
@ -1976,10 +1967,10 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 600px; /* 设置最小高度确保容器足够大 */
|
||||
min-height: 600px; /* Set minimum height to ensure container is large enough */
|
||||
}
|
||||
|
||||
/* 示例图片区块 */
|
||||
/* Sample images section */
|
||||
.sample-images-section {
|
||||
margin-top: 24px;
|
||||
background: white;
|
||||
@ -2099,7 +2090,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 600px; /* 设置最小高度 */
|
||||
min-height: 600px; /* Set minimum height */
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@ -2113,7 +2104,7 @@ onUnmounted(() => {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
min-height: 600px; /* 确保预览区域有足够高度 */
|
||||
min-height: 600px; /* Ensure preview area has sufficient height */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
@ -51,10 +51,10 @@
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 功能设置卡片 -->
|
||||
<!-- Feature settings card -->
|
||||
<n-card>
|
||||
<n-list>
|
||||
<!-- 应用市场开发者 -->
|
||||
<!-- Marketplace developer -->
|
||||
<n-list-item v-if="!teamInfo?.is_developer">
|
||||
<n-thing>
|
||||
<template #header>
|
||||
@ -73,7 +73,7 @@
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
|
||||
<!-- 双因素认证 -->
|
||||
<!-- Two-factor authentication -->
|
||||
<n-list-item>
|
||||
<n-thing>
|
||||
<template #header>
|
||||
@ -90,7 +90,7 @@
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
|
||||
<!-- 重置密码 -->
|
||||
<!-- Reset password -->
|
||||
<n-list-item>
|
||||
<n-thing>
|
||||
<template #header>
|
||||
@ -110,7 +110,7 @@
|
||||
</n-list>
|
||||
</n-card>
|
||||
|
||||
<!-- 推荐有礼 -->
|
||||
<!-- Referral program -->
|
||||
<n-card v-if="referralLink">
|
||||
<n-list>
|
||||
<n-list-item>
|
||||
@ -137,7 +137,7 @@
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- Jingrow 合作伙伴 -->
|
||||
<!-- Jingrow Partner -->
|
||||
<n-card v-if="!teamInfo?.jerp_partner">
|
||||
<n-list>
|
||||
<n-list-item>
|
||||
@ -192,10 +192,10 @@
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 开发者标签页 -->
|
||||
<!-- Developer tab -->
|
||||
<n-tab-pane name="developer" :tab="t('Developer')">
|
||||
<n-space vertical :size="24">
|
||||
<!-- API 访问 -->
|
||||
<!-- API Access -->
|
||||
<n-card :title="t('API Access')">
|
||||
<n-space vertical :size="20">
|
||||
<div class="api-description">
|
||||
@ -215,7 +215,7 @@
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- SSH 密钥 -->
|
||||
<!-- SSH Keys -->
|
||||
<n-card :title="t('SSH Keys')">
|
||||
<n-space vertical :size="16">
|
||||
<div class="ssh-actions">
|
||||
@ -237,7 +237,7 @@
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 功能标志(仅管理员可见) -->
|
||||
<!-- Feature flags (admin only) -->
|
||||
<n-card v-if="isAdmin" :title="t('Advanced Features')">
|
||||
<n-form :model="featureFlags" label-placement="left" label-width="200px">
|
||||
<n-form-item
|
||||
@ -268,10 +268,10 @@
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 系统设置标签页 -->
|
||||
<!-- System settings tab -->
|
||||
<n-tab-pane name="system" :tab="t('System Settings')">
|
||||
<n-grid :cols="2" :x-gap="24" :y-gap="24">
|
||||
<!-- 左栏:系统设置 -->
|
||||
<!-- Left column: System settings -->
|
||||
<n-grid-item>
|
||||
<n-card :title="t('System Settings')">
|
||||
<n-form :model="systemSettings" label-placement="left" label-width="120px">
|
||||
@ -320,7 +320,7 @@
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
|
||||
<!-- 右栏:环境配置(仅系统管理员可见) -->
|
||||
<!-- Right column: Environment configuration (system admin only) -->
|
||||
<n-grid-item v-if="isAdmin">
|
||||
<n-card :title="t('Environment Configuration')">
|
||||
<n-alert type="warning" style="margin-bottom: 16px">
|
||||
@ -455,7 +455,7 @@
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- 编辑个人资料对话框 -->
|
||||
<!-- Edit profile dialog -->
|
||||
<n-modal v-model:show="showProfileEditDialog" preset="card" :title="t('Update Profile Information')" :style="{ width: '600px' }">
|
||||
<n-form :model="profileForm" label-placement="top">
|
||||
<n-space vertical :size="20">
|
||||
@ -480,7 +480,7 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 创建 API Secret 对话框 -->
|
||||
<!-- Create API Secret dialog -->
|
||||
<n-modal v-model:show="showCreateSecretDialog" preset="card" :title="t('API Access')" :style="{ width: '700px' }">
|
||||
<n-space vertical :size="20">
|
||||
<div v-if="!createSecretData">
|
||||
@ -515,7 +515,7 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 添加 SSH 密钥对话框 -->
|
||||
<!-- Add SSH key dialog -->
|
||||
<n-modal v-model:show="showAddSSHKeyDialog" preset="card" :title="t('Add New SSH Key')" :style="{ width: '700px' }">
|
||||
<n-space vertical :size="20">
|
||||
<p class="text-base">{{ t('Add a new SSH key to your account') }}</p>
|
||||
@ -543,11 +543,11 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 双因素认证对话框 -->
|
||||
<!-- Two-factor authentication dialog -->
|
||||
<n-modal v-model:show="show2FADialog" preset="card" :title="twoFactorAuthTitle" :style="{ width: '700px' }">
|
||||
<n-spin :show="loadingQRCode">
|
||||
<n-space vertical :size="24">
|
||||
<!-- 禁用 2FA 模式 -->
|
||||
<!-- Disable 2FA mode -->
|
||||
<div v-if="is2FAEnabled">
|
||||
<n-alert
|
||||
type="error"
|
||||
@ -573,7 +573,7 @@
|
||||
|
||||
<!-- 启用 2FA 模式 -->
|
||||
<div v-else>
|
||||
<!-- QR 码 - 居中显示 -->
|
||||
<!-- QR code - centered display -->
|
||||
<div class="tfa-qr-container" v-if="qrCodeUrl">
|
||||
<img
|
||||
:src="`https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${encodeURIComponent(qrCodeUrl)}`"
|
||||
@ -582,7 +582,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 步骤说明 -->
|
||||
<!-- Step instructions -->
|
||||
<n-card>
|
||||
<h3 class="text-lg font-semibold mb-3">{{ t('Steps to Enable Two-Factor Authentication') }}</h3>
|
||||
<ol class="ml-4 list-decimal space-y-2 text-sm text-gray-700 mb-4">
|
||||
@ -606,7 +606,7 @@
|
||||
</p>
|
||||
</n-card>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<!-- Verification code input -->
|
||||
<n-form-item :label="t('Verify the code in the app to enable two-factor authentication')" class="mt-6">
|
||||
<n-input
|
||||
v-model:value="totpCode"
|
||||
@ -617,14 +617,14 @@
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<!-- Error message -->
|
||||
<n-alert
|
||||
v-if="twoFAError"
|
||||
type="error"
|
||||
:title="twoFAError"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<!-- Action buttons -->
|
||||
<n-button
|
||||
v-if="!is2FAEnabled"
|
||||
type="primary"
|
||||
@ -656,7 +656,7 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 重置密码对话框 -->
|
||||
<!-- Reset password dialog -->
|
||||
<n-modal v-model:show="showResetPasswordDialog" preset="card" :title="t('Reset Password')" :style="{ width: '700px' }">
|
||||
<n-space vertical :size="20">
|
||||
<n-form-item :label="t('Current Password')">
|
||||
@ -719,7 +719,7 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 添加合作伙伴代码对话框 -->
|
||||
<!-- Add partner code dialog -->
|
||||
<n-modal v-model:show="showAddPartnerCodeDialog" preset="card" :title="t('Link Partner Account')" :style="{ width: '700px' }">
|
||||
<n-space vertical :size="20">
|
||||
<p class="text-base">{{ t('Enter the partner code provided by your partner') }}</p>
|
||||
@ -756,7 +756,7 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 移除合作伙伴对话框 -->
|
||||
<!-- Remove partner dialog -->
|
||||
<n-modal
|
||||
v-model:show="showRemovePartnerDialog"
|
||||
preset="dialog"
|
||||
@ -838,25 +838,25 @@ const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 当前激活的标签页
|
||||
// Currently active tab
|
||||
const activeTab = ref('profile')
|
||||
|
||||
// 检查是否为系统管理员
|
||||
// Check if system administrator
|
||||
const isAdmin = computed(() => {
|
||||
const user = authStore.user
|
||||
return user?.user === 'Administrator' || user?.user_type === 'System User'
|
||||
})
|
||||
|
||||
// 检查是否为 local 运行模式
|
||||
// Check if local run mode
|
||||
const isLocalMode = computed(() => {
|
||||
return envConfig.run_mode === 'local'
|
||||
})
|
||||
|
||||
// 用户账户信息
|
||||
// User account information
|
||||
const userAccountInfo = ref<any>(null)
|
||||
const userAccountInfoLoading = ref(false)
|
||||
|
||||
// 个人资料相关
|
||||
// Profile related
|
||||
const showProfileEditDialog = ref(false)
|
||||
const profileForm = reactive({
|
||||
first_name: '',
|
||||
@ -867,10 +867,10 @@ const profileForm = reactive({
|
||||
})
|
||||
const profileSaving = ref(false)
|
||||
|
||||
// Team 信息
|
||||
// Team information
|
||||
const teamInfo = ref<any>(null)
|
||||
|
||||
// 推荐有礼
|
||||
// Referral program
|
||||
const referralLink = computed(() => {
|
||||
if (teamInfo.value?.referrer_id) {
|
||||
return `${location.origin}/dashboard/signup?referrer=${teamInfo.value.referrer_id}`
|
||||
@ -878,7 +878,7 @@ const referralLink = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
// 合作伙伴
|
||||
// Partner
|
||||
const showAddPartnerCodeDialog = ref(false)
|
||||
const showRemovePartnerDialog = ref(false)
|
||||
const partnerCode = ref('')
|
||||
@ -888,7 +888,7 @@ const partnerName = ref('')
|
||||
const partnerCodeError = ref('')
|
||||
const addPartnerCodeLoading = ref(false)
|
||||
|
||||
// 重置密码
|
||||
// Reset password
|
||||
const oldPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
@ -899,7 +899,7 @@ const passwordError = ref('')
|
||||
const resetPasswordLoading = ref(false)
|
||||
const passwordStrengthTimeout = ref<any>(null)
|
||||
|
||||
// 双因素认证
|
||||
// Two-factor authentication
|
||||
const show2FADialog = ref(false)
|
||||
const qrCodeUrl = ref('')
|
||||
const totpCode = ref('')
|
||||
@ -927,10 +927,10 @@ const loading2FA = ref(false)
|
||||
const loadingQRCode = ref(false)
|
||||
const twoFAError = ref('')
|
||||
|
||||
// 重置密码
|
||||
// Reset password
|
||||
const showResetPasswordDialog = ref(false)
|
||||
|
||||
// API Secret 相关
|
||||
// API Secret related
|
||||
const showCreateSecretDialog = ref(false)
|
||||
const createSecretData = ref<{ api_key: string; api_secret: string } | null>(null)
|
||||
const createSecretLoading = ref(false)
|
||||
@ -938,7 +938,7 @@ const apiKeyButtonLabel = computed(() => {
|
||||
return userAccountInfo.value?.api_key ? t('Regenerate API Key') : t('Create New API Key')
|
||||
})
|
||||
|
||||
// SSH 密钥相关
|
||||
// SSH key related
|
||||
const sshKeys = ref<any[]>([])
|
||||
const sshKeysLoading = ref(false)
|
||||
const showAddSSHKeyDialog = ref(false)
|
||||
@ -949,7 +949,7 @@ const sshKeyPlaceholder = computed(() => {
|
||||
return t("Starts with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'")
|
||||
})
|
||||
|
||||
// SSH 密钥表格列
|
||||
// SSH key table columns
|
||||
const sshKeyColumns = [
|
||||
{
|
||||
title: t('SSH Fingerprint'),
|
||||
@ -984,7 +984,7 @@ const sshKeyColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
// 功能标志相关
|
||||
// Feature flags related
|
||||
const featureFlags = reactive<Record<string, boolean>>({})
|
||||
const featureFlagsSaving = ref(false)
|
||||
const featureFlagFields = [
|
||||
@ -992,11 +992,11 @@ const featureFlagFields = [
|
||||
{ label: t('Enable security portal'), fieldname: 'security_portal_enabled' }
|
||||
]
|
||||
const featureFlagsDirty = computed(() => {
|
||||
// 这里需要根据实际数据判断是否有变更
|
||||
// Need to check if there are changes based on actual data
|
||||
return false
|
||||
})
|
||||
|
||||
// 环境配置
|
||||
// Environment configuration
|
||||
const envConfig = reactive<Partial<EnvironmentConfig>>({})
|
||||
const envConfigLoading = ref(false)
|
||||
const envConfigSaving = ref(false)
|
||||
@ -1009,13 +1009,13 @@ const systemSettings = reactive({
|
||||
timezone: getCurrentTimezone()
|
||||
})
|
||||
|
||||
// 语言选项
|
||||
// Language options
|
||||
const languageOptions = locales.map(locale => ({
|
||||
label: `${locale.flag} ${locale.name}`,
|
||||
value: locale.code
|
||||
}))
|
||||
|
||||
// 每页数量选项
|
||||
// Items per page options
|
||||
const pageSizeOptions = [
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
@ -1023,30 +1023,30 @@ const pageSizeOptions = [
|
||||
{ label: '100', value: 100 }
|
||||
]
|
||||
|
||||
// 时区选项 - 使用标准的 IANA 时区列表,按 UTC 偏移量分组(符合业内最佳实践)
|
||||
// Timezone options - use standard IANA timezone list, grouped by UTC offset (industry best practice)
|
||||
const timezoneOptions = ref<Array<{ type: string; label: string; key: string; children: Array<{ label: string; value: string }> }>>([])
|
||||
const timezoneError = ref<string | null>(null)
|
||||
|
||||
// 数据库类型选项
|
||||
// Database type options
|
||||
const dbTypeOptions = [
|
||||
{ label: 'MariaDB', value: 'mariadb' },
|
||||
{ label: 'MySQL', value: 'mysql' },
|
||||
{ label: 'PostgreSQL', value: 'postgresql' }
|
||||
]
|
||||
|
||||
// 运行模式选项
|
||||
// Run mode options
|
||||
const runModeOptions = [
|
||||
{ label: 'API', value: 'api' },
|
||||
{ label: 'Local', value: 'local' }
|
||||
]
|
||||
|
||||
// 环境选项
|
||||
// Environment options
|
||||
const environmentOptions = [
|
||||
{ label: 'Development', value: 'development' },
|
||||
{ label: 'Production', value: 'production' }
|
||||
]
|
||||
|
||||
// 日志级别选项
|
||||
// Log level options
|
||||
const logLevelOptions = [
|
||||
{ label: 'DEBUG', value: 'DEBUG' },
|
||||
{ label: 'INFO', value: 'INFO' },
|
||||
@ -1061,21 +1061,21 @@ const changeLanguage = (locale: string) => {
|
||||
}
|
||||
|
||||
const saveSystemSettings = () => {
|
||||
// 保存应用名称
|
||||
// Save app name
|
||||
localStorage.setItem('appName', systemSettings.appName)
|
||||
// 保存每页数量设置
|
||||
// Save items per page setting
|
||||
localStorage.setItem('itemsPerPage', systemSettings.itemsPerPage.toString())
|
||||
// 保存时区设置
|
||||
// Save timezone setting
|
||||
localStorage.setItem('timezone', systemSettings.timezone)
|
||||
message.success(t('System settings saved'))
|
||||
|
||||
// 保存成功后自动刷新页面,让新设置生效
|
||||
// Auto refresh page after saving to apply new settings
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 加载环境配置
|
||||
// Load environment configuration
|
||||
const loadEnvironmentConfig = async (showMessage = true) => {
|
||||
if (!isAdmin.value) {
|
||||
return
|
||||
@ -1103,7 +1103,7 @@ const loadEnvironmentConfig = async (showMessage = true) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存环境配置
|
||||
// Save environment configuration
|
||||
const saveEnvironmentConfig = async () => {
|
||||
if (!isAdmin.value) {
|
||||
message.error(t('Only system administrators can edit environment configuration'))
|
||||
@ -1115,7 +1115,7 @@ const saveEnvironmentConfig = async () => {
|
||||
const result = await updateEnvironmentConfig(envConfig)
|
||||
if (result.success) {
|
||||
message.success(result.message || t('Environment configuration saved'))
|
||||
// 重新加载配置以获取最新值(静默加载,不显示消息)
|
||||
// Reload configuration to get latest values (silent load, no message)
|
||||
await loadEnvironmentConfig(false)
|
||||
} else {
|
||||
message.error(result.message || t('Failed to save environment configuration'))
|
||||
@ -1127,7 +1127,7 @@ const saveEnvironmentConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 重启环境
|
||||
// Restart environment
|
||||
const handleRestartEnvironment = () => {
|
||||
if (!isAdmin.value) {
|
||||
message.error(t('Only system administrators can restart environment'))
|
||||
@ -1158,7 +1158,7 @@ const handleRestartEnvironment = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 加载用户账户信息
|
||||
// Load user account information
|
||||
const loadUserAccountInfo = async () => {
|
||||
userAccountInfoLoading.value = true
|
||||
try {
|
||||
@ -1173,22 +1173,22 @@ const loadUserAccountInfo = async () => {
|
||||
profileForm.email = result.data.email || ''
|
||||
}
|
||||
|
||||
// 同时获取 Team 信息
|
||||
// Also get Team information
|
||||
if (result.team) {
|
||||
teamInfo.value = result.team
|
||||
// 加载合作伙伴名称
|
||||
// Load partner name
|
||||
if (result.team.partner_email) {
|
||||
await loadPartnerName()
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载用户账户信息失败:', error)
|
||||
// Failed to load user account information
|
||||
} finally {
|
||||
userAccountInfoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存个人资料
|
||||
// Save profile
|
||||
const saveProfile = async () => {
|
||||
profileSaving.value = true
|
||||
try {
|
||||
@ -1211,7 +1211,7 @@ const saveProfile = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 成为开发者
|
||||
// Become developer
|
||||
const handleBecomeDeveloper = () => {
|
||||
dialog.warning({
|
||||
title: t('Become a Marketplace Developer?'),
|
||||
@ -1220,25 +1220,25 @@ const handleBecomeDeveloper = () => {
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
if (!teamInfo.value?.name) {
|
||||
message.error(t('无法获取团队信息'))
|
||||
message.error(t('Unable to get team information'))
|
||||
return
|
||||
}
|
||||
const result = await becomeDeveloper(teamInfo.value.name)
|
||||
if (result.success) {
|
||||
message.success(t('You can now publish apps to our marketplace'))
|
||||
// set_value API 返回更新后的 Team 对象,直接更新 teamInfo 的 is_developer 字段
|
||||
// set_value API returns updated Team object, directly update teamInfo's is_developer field
|
||||
if (result.data) {
|
||||
// 确保 is_developer 字段被正确设置
|
||||
// Ensure is_developer field is set correctly
|
||||
teamInfo.value = {
|
||||
...teamInfo.value,
|
||||
...result.data,
|
||||
is_developer: result.data.is_developer !== undefined ? result.data.is_developer : 1
|
||||
}
|
||||
} else {
|
||||
// 如果 API 没有返回数据,直接设置 is_developer
|
||||
// If API doesn't return data, directly set is_developer
|
||||
teamInfo.value = { ...teamInfo.value, is_developer: 1 }
|
||||
}
|
||||
// 延迟一下再刷新,避免缓存问题(API 有 60 秒缓存)
|
||||
// Delay refresh to avoid cache issues (API has 60 second cache)
|
||||
setTimeout(async () => {
|
||||
await loadUserAccountInfo()
|
||||
}, 1000)
|
||||
@ -1249,7 +1249,7 @@ const handleBecomeDeveloper = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 移除合作伙伴
|
||||
// Remove partner
|
||||
const handleRemovePartner = async () => {
|
||||
const result = await removePartner()
|
||||
if (result.success) {
|
||||
@ -1261,7 +1261,7 @@ const handleRemovePartner = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查密码强度
|
||||
// Check password strength
|
||||
const checkPasswordStrength = () => {
|
||||
if (passwordStrengthTimeout.value) {
|
||||
clearTimeout(passwordStrengthTimeout.value)
|
||||
@ -1302,7 +1302,7 @@ const checkPasswordStrength = () => {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 检查密码不匹配
|
||||
// Check password mismatch
|
||||
const checkPasswordMismatch = () => {
|
||||
if (oldPassword.value && newPassword.value && oldPassword.value === newPassword.value) {
|
||||
passwordMismatchMessage.value = t('New password cannot be the same as current password')
|
||||
@ -1315,7 +1315,7 @@ const checkPasswordMismatch = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码表单验证
|
||||
// Reset password form validation
|
||||
const isResetPasswordFormValid = computed(() => {
|
||||
const hasNewPassword = newPassword.value && newPassword.value.length > 0
|
||||
const hasConfirmPassword = confirmPassword.value && confirmPassword.value.length > 0
|
||||
@ -1325,7 +1325,7 @@ const isResetPasswordFormValid = computed(() => {
|
||||
return oldPassword.value && hasNewPassword && hasConfirmPassword && passwordsMatch && passwordsDifferent
|
||||
})
|
||||
|
||||
// 处理重置密码
|
||||
// Handle reset password
|
||||
const handleResetPassword = async () => {
|
||||
passwordError.value = ''
|
||||
|
||||
@ -1397,7 +1397,7 @@ const closeResetPasswordDialog = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 合作伙伴代码变化处理(防抖)
|
||||
// Partner code change handler (debounce)
|
||||
let partnerCodeDebounceTimer: any = null
|
||||
const handlePartnerCodeChange = () => {
|
||||
partnerExists.value = false
|
||||
@ -1427,7 +1427,7 @@ const handlePartnerCodeChange = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 添加合作伙伴代码
|
||||
// Add partner code
|
||||
const handleAddPartnerCode = async () => {
|
||||
if (!partnerExists.value) {
|
||||
return
|
||||
@ -1454,7 +1454,7 @@ const handleAddPartnerCode = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭添加合作伙伴代码对话框
|
||||
// Close add partner code dialog
|
||||
const closeAddPartnerCodeDialog = () => {
|
||||
showAddPartnerCodeDialog.value = false
|
||||
partnerCode.value = ''
|
||||
@ -1467,7 +1467,7 @@ const closeAddPartnerCodeDialog = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载合作伙伴名称
|
||||
// Load partner name
|
||||
const loadPartnerName = async () => {
|
||||
if (teamInfo.value?.partner_email) {
|
||||
const result = await getPartnerName(teamInfo.value.partner_email)
|
||||
@ -1477,10 +1477,10 @@ const loadPartnerName = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载 2FA QR 码
|
||||
// Load 2FA QR code
|
||||
const load2FAQRCode = async () => {
|
||||
if (is2FAEnabled.value) {
|
||||
return // 如果已启用,不需要加载 QR 码
|
||||
return // If already enabled, no need to load QR code
|
||||
}
|
||||
|
||||
loadingQRCode.value = true
|
||||
@ -1498,7 +1498,7 @@ const load2FAQRCode = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 启用 2FA
|
||||
// Enable 2FA
|
||||
const handleEnable2FA = async () => {
|
||||
if (!totpCode.value) {
|
||||
twoFAError.value = t('Please enter the code from the authenticator app')
|
||||
@ -1514,7 +1514,7 @@ const handleEnable2FA = async () => {
|
||||
message.success(t('Two-factor authentication enabled successfully'))
|
||||
totpCode.value = ''
|
||||
close2FADialog()
|
||||
// 重新加载用户信息
|
||||
// Reload user information
|
||||
setTimeout(async () => {
|
||||
await loadUserAccountInfo()
|
||||
}, 500)
|
||||
@ -1536,7 +1536,7 @@ const handleEnable2FA = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用 2FA
|
||||
// Disable 2FA
|
||||
const handleDisable2FA = async () => {
|
||||
if (!totpCode.value) {
|
||||
twoFAError.value = t('Please enter the code from the authenticator app')
|
||||
@ -1574,7 +1574,7 @@ const handleDisable2FA = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 2FA 对话框
|
||||
// Close 2FA dialog
|
||||
const close2FADialog = () => {
|
||||
show2FADialog.value = false
|
||||
totpCode.value = ''
|
||||
@ -1583,7 +1583,7 @@ const close2FADialog = () => {
|
||||
showSetupKey.value = false
|
||||
}
|
||||
|
||||
// 监听 2FA 对话框打开,加载 QR 码
|
||||
// Watch 2FA dialog open, load QR code
|
||||
watch(show2FADialog, (newVal) => {
|
||||
if (newVal && !is2FAEnabled.value) {
|
||||
load2FAQRCode()
|
||||
@ -1591,7 +1591,7 @@ watch(show2FADialog, (newVal) => {
|
||||
})
|
||||
|
||||
|
||||
// 创建 API Secret
|
||||
// Create API Secret
|
||||
const handleCreateSecret = async () => {
|
||||
createSecretLoading.value = true
|
||||
try {
|
||||
@ -1610,14 +1610,14 @@ const handleCreateSecret = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭创建 Secret 对话框
|
||||
// Close create Secret dialog
|
||||
const closeCreateSecretDialog = () => {
|
||||
showCreateSecretDialog.value = false
|
||||
createSecretData.value = null
|
||||
loadUserAccountInfo()
|
||||
}
|
||||
|
||||
// 加载 SSH 密钥列表
|
||||
// Load SSH key list
|
||||
const loadSSHKeys = async () => {
|
||||
sshKeysLoading.value = true
|
||||
try {
|
||||
@ -1626,13 +1626,13 @@ const loadSSHKeys = async () => {
|
||||
sshKeys.value = result.data
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载 SSH 密钥列表失败:', error)
|
||||
// Failed to load SSH key list
|
||||
} finally {
|
||||
sshKeysLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 SSH 密钥
|
||||
// Add SSH key
|
||||
const handleAddSSHKey = async () => {
|
||||
if (!sshKeyValue.value.trim()) {
|
||||
sshKeyError.value = t('SSH key is required')
|
||||
@ -1658,7 +1658,7 @@ const handleAddSSHKey = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认 SSH 密钥
|
||||
// Set default SSH key
|
||||
const handleSetDefaultSSHKey = async (keyName: string) => {
|
||||
try {
|
||||
const result = await markKeyAsDefault(keyName)
|
||||
@ -1673,7 +1673,7 @@ const handleSetDefaultSSHKey = async (keyName: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 SSH 密钥
|
||||
// Delete SSH key
|
||||
const handleDeleteSSHKey = async (keyName: string) => {
|
||||
dialog.warning({
|
||||
title: t('Delete SSH Key'),
|
||||
@ -1732,25 +1732,24 @@ onMounted(async () => {
|
||||
initLocale()
|
||||
systemSettings.language = getCurrentLocale()
|
||||
|
||||
// 初始化时区选项(使用分组显示,符合业内最佳实践)
|
||||
// Initialize timezone options (use grouped display, industry best practice)
|
||||
try {
|
||||
timezoneOptions.value = getGroupedTimezoneOptions()
|
||||
} catch (error) {
|
||||
timezoneError.value = error instanceof Error ? error.message : String(error)
|
||||
message.error(t('Failed to load timezone options') + ': ' + timezoneError.value)
|
||||
console.error('Failed to load timezone options:', error)
|
||||
}
|
||||
|
||||
// 加载用户账户信息(包含 Team 信息)
|
||||
// Load user account information (including Team information)
|
||||
await loadUserAccountInfo()
|
||||
|
||||
// 加载 SSH 密钥列表
|
||||
// Load SSH key list
|
||||
await loadSSHKeys()
|
||||
|
||||
// 加载功能标志
|
||||
// Load feature flags
|
||||
await loadFeatureFlags()
|
||||
|
||||
// 如果是系统管理员,加载环境配置(静默加载,不显示消息)
|
||||
// If system administrator, load environment configuration (silent load, no message)
|
||||
if (isAdmin.value) {
|
||||
await loadEnvironmentConfig(false)
|
||||
}
|
||||
|
||||
@ -218,11 +218,11 @@ const urlInputRef = ref<HTMLInputElement | null>(null)
|
||||
const uploadedImage = ref<File | null>(null)
|
||||
const uploadedImageUrl = ref<string>('')
|
||||
const resultImage = ref<string>('')
|
||||
const resultImageBlobUrl = ref<string>('') // 缓存的 blob URL,用于下载
|
||||
const resultImageBlobUrl = ref<string>('') // Cached blob URL for download
|
||||
const imageUrl = ref<string>('')
|
||||
const resultImageUrl = computed(() => {
|
||||
if (!resultImage.value) return ''
|
||||
// 直接返回图片URL
|
||||
// Return image URL directly
|
||||
return resultImage.value
|
||||
})
|
||||
|
||||
@ -282,7 +282,7 @@ const handlePaste = async (event: ClipboardEvent) => {
|
||||
const items = event.clipboardData?.items
|
||||
if (!items) return
|
||||
|
||||
// 检查是否有图片数据
|
||||
// Check if there is image data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
@ -295,7 +295,7 @@ const handlePaste = async (event: ClipboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有文本URL
|
||||
// Check if there is text URL
|
||||
const text = event.clipboardData?.getData('text')
|
||||
if (text && isValidImageUrl(text)) {
|
||||
event.preventDefault()
|
||||
@ -351,7 +351,7 @@ const handleUrlSubmit = async () => {
|
||||
currentHistoryIndex.value = -1
|
||||
|
||||
try {
|
||||
// 使用代理或直接加载图片
|
||||
// Use proxy or load image directly
|
||||
const response = await fetch(url, { mode: 'cors' })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load image: ${response.status}`)
|
||||
@ -359,7 +359,7 @@ const handleUrlSubmit = async () => {
|
||||
|
||||
const blob = await response.blob()
|
||||
|
||||
// 验证文件类型
|
||||
// Validate file type
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||
if (!validTypes.includes(blob.type)) {
|
||||
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
|
||||
@ -367,7 +367,7 @@ const handleUrlSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
// Validate file size
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (blob.size > maxSize) {
|
||||
message.warning(t('Image size exceeds 10MB limit'))
|
||||
@ -375,10 +375,10 @@ const handleUrlSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为File对象
|
||||
// Convert to File object
|
||||
const originalFile = new File([blob], 'image-from-url', { type: blob.type })
|
||||
|
||||
// 压缩图片到 1024x1024
|
||||
// Compress image to 1024x1024
|
||||
try {
|
||||
const compressedFile = await compressImageFile(originalFile, {
|
||||
maxWidth: 1024,
|
||||
@ -390,15 +390,13 @@ const handleUrlSubmit = async () => {
|
||||
uploadedImage.value = compressedFile
|
||||
uploadedImageUrl.value = URL.createObjectURL(compressedFile)
|
||||
|
||||
// 开始处理
|
||||
// Start processing
|
||||
await handleRemoveBackground()
|
||||
} catch (error) {
|
||||
console.error('图片压缩失败:', error)
|
||||
message.error('图片处理失败,请重试')
|
||||
message.error(t('Image processing failed, please try again'))
|
||||
processing.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载图片URL失败:', error)
|
||||
let errorMessage = t('Failed to load image from URL')
|
||||
|
||||
if (error.message?.includes('CORS')) {
|
||||
@ -420,11 +418,11 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('paste', handlePaste)
|
||||
// 清理对象URL
|
||||
// Clean up object URL
|
||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||
}
|
||||
// 清理结果图片的 blob URL 缓存
|
||||
// Clean up result image blob URL cache
|
||||
if (resultImageBlobUrl.value) {
|
||||
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||
}
|
||||
@ -497,7 +495,7 @@ const processFile = async (file: File) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 压缩图片到 1024x1024
|
||||
// Compress image to 1024x1024
|
||||
try {
|
||||
const compressedFile = await compressImageFile(file, {
|
||||
maxWidth: 1024,
|
||||
@ -518,17 +516,16 @@ const processFile = async (file: File) => {
|
||||
}
|
||||
reader.readAsDataURL(compressedFile)
|
||||
} catch (error) {
|
||||
console.error('图片压缩失败:', error)
|
||||
message.error('图片处理失败,请重试')
|
||||
message.error(t('Image processing failed, please try again'))
|
||||
}
|
||||
}
|
||||
|
||||
const resetUpload = () => {
|
||||
// 清理对象URL
|
||||
// Clean up object URL
|
||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||
}
|
||||
// 清理结果图片的 blob URL 缓存
|
||||
// Clean up result image blob URL cache
|
||||
if (resultImageBlobUrl.value) {
|
||||
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||
resultImageBlobUrl.value = ''
|
||||
@ -558,7 +555,7 @@ const selectHistoryItem = async (index: number) => {
|
||||
splitPosition.value = 0
|
||||
uploadedImage.value = item.originalImageFile
|
||||
|
||||
// 缓存历史记录的结果图片
|
||||
// Cache result image from history
|
||||
if (item.resultImage) {
|
||||
await cacheResultImage(item.resultImage)
|
||||
}
|
||||
@ -608,7 +605,7 @@ const handleRemoveBackground = async () => {
|
||||
processing.value = true
|
||||
resultImage.value = ''
|
||||
|
||||
// 处理成功结果的辅助函数(文件内部使用)
|
||||
// Helper function to handle successful result (internal use)
|
||||
const handleSuccess = async (imageUrl: string): Promise<void> => {
|
||||
resultImage.value = imageUrl
|
||||
await cacheResultImage(imageUrl)
|
||||
@ -674,13 +671,13 @@ const handleRemoveBackground = async () => {
|
||||
message.error(result.error)
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON:', parseError, 'Line:', line)
|
||||
// Failed to parse JSON, continue processing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一行
|
||||
// Process last line
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const result = JSON.parse(buffer.trim())
|
||||
@ -691,7 +688,6 @@ const handleRemoveBackground = async () => {
|
||||
message.error(result.error || t('Failed to remove background'))
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse final JSON:', parseError)
|
||||
message.error(t('Failed to parse response'))
|
||||
}
|
||||
} else {
|
||||
@ -713,17 +709,17 @@ const handleRemoveBackground = async () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存结果图片为 blob URL,用于下载
|
||||
* Cache result image as blob URL for download
|
||||
*/
|
||||
const cacheResultImage = async (imageUrl: string) => {
|
||||
try {
|
||||
// 清理旧的 blob URL
|
||||
// Clean up old blob URL
|
||||
if (resultImageBlobUrl.value) {
|
||||
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||
resultImageBlobUrl.value = ''
|
||||
}
|
||||
|
||||
// 获取图片并转换为 blob URL
|
||||
// Fetch image and convert to blob URL
|
||||
const response = await fetch(imageUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status}`)
|
||||
@ -731,8 +727,7 @@ const cacheResultImage = async (imageUrl: string) => {
|
||||
const blob = await response.blob()
|
||||
resultImageBlobUrl.value = URL.createObjectURL(blob)
|
||||
} catch (error) {
|
||||
console.error('缓存图片失败:', error)
|
||||
// 缓存失败不影响显示,只是下载时需要重新获取
|
||||
// Cache failure doesn't affect display, only download needs to refetch
|
||||
}
|
||||
}
|
||||
|
||||
@ -740,10 +735,10 @@ const handleDownload = async () => {
|
||||
if (!resultImage.value) return
|
||||
|
||||
try {
|
||||
// 优先使用缓存的 blob URL
|
||||
// Prefer cached blob URL
|
||||
let blobUrl = resultImageBlobUrl.value
|
||||
|
||||
// 如果没有缓存,则获取并缓存
|
||||
// If no cache, fetch and cache
|
||||
if (!blobUrl) {
|
||||
const response = await fetch(resultImage.value)
|
||||
if (!response.ok) {
|
||||
@ -754,7 +749,7 @@ const handleDownload = async () => {
|
||||
resultImageBlobUrl.value = blobUrl
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
// Create download link
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = `removed-background-${Date.now()}.png`
|
||||
@ -763,12 +758,11 @@ const handleDownload = async () => {
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
// 清理:移除 DOM 元素,但不释放 blob URL(保留缓存供后续下载使用)
|
||||
// Clean up: remove DOM element but don't release blob URL (keep cache for subsequent downloads)
|
||||
requestAnimationFrame(() => {
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('下载图片失败:', error)
|
||||
message.error(t('Failed to download image'))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user