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:
jingrow 2026-01-04 22:19:19 +08:00
parent 36af166879
commit ee2f59df81
4 changed files with 213 additions and 224 deletions

View File

@ -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": "无法获取团队信息"
} }

View File

@ -17,7 +17,7 @@ const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
const currentYear = computed(() => new Date().getFullYear()) const currentYear = computed(() => new Date().getFullYear())
const logoUrl = computed(() => '/logo.svg') const logoUrl = computed(() => '/logo.svg')
// / // Login/Signup modal state
const showLoginModal = ref(false) const showLoginModal = ref(false)
const showSignupModal = ref(false) const showSignupModal = ref(false)
const loginFormRef = ref() const loginFormRef = ref()
@ -26,7 +26,7 @@ const loginLoading = ref(false)
const signupLoading = ref(false) const signupLoading = ref(false)
const showSignupLink = ref(true) const showSignupLink = ref(true)
// // Login form
const loginFormData = reactive({ const loginFormData = reactive({
username: '', username: '',
password: '' password: ''
@ -42,7 +42,7 @@ const loginRules = {
] ]
} }
// // Signup form
const signupFormData = reactive({ const signupFormData = reactive({
username: '', username: '',
password: '', password: '',
@ -76,13 +76,13 @@ const signupRules = computed(() => {
] ]
} }
// email // English version: email required, phone optional
if (isEnglish.value) { if (isEnglish.value) {
rules.email = [ rules.email = [
{ required: true, message: t('Please enter email'), trigger: 'blur' }, { required: true, message: t('Please enter email'), trigger: 'blur' },
{ {
validator: (_rule: any, value: string) => { validator: (_rule: any, value: string) => {
// required // Required rule handles empty values, only validate format here
if (!value) return true if (!value) return true
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) { if (!emailRegex.test(value)) {
@ -107,7 +107,7 @@ const signupRules = computed(() => {
} }
] ]
} else { } else {
// email // Chinese version: email optional, phone required
rules.email = [ rules.email = [
{ {
validator: (_rule: any, value: string) => { validator: (_rule: any, value: string) => {
@ -205,11 +205,9 @@ const handleSignupSubmit = async () => {
} }
} else { } else {
const errorMsg = result.error || t('Sign up failed') const errorMsg = result.error || t('Sign up failed')
console.error('注册失败:', errorMsg, result)
message.error(errorMsg) message.error(errorMsg)
} }
} catch (error: any) { } catch (error: any) {
console.error('注册异常:', error)
message.error(error.message || t('Sign up failed, please try again')) message.error(error.message || t('Sign up failed, please try again'))
} finally { } finally {
signupLoading.value = false signupLoading.value = false
@ -226,15 +224,15 @@ const switchToLogin = () => {
showLoginModal.value = true showLoginModal.value = true
} }
// // Login state
const isLoggedIn = computed(() => authStore.isLoggedIn) const isLoggedIn = computed(() => authStore.isLoggedIn)
// Sidebar // Sidebar collapse state
const SIDEBAR_COLLAPSE_KEY = 'app.sidebar.collapsed' const SIDEBAR_COLLAPSE_KEY = 'app.sidebar.collapsed'
const collapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSE_KEY) === 'true') const collapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSE_KEY) === 'true')
const isMobile = ref(false) const isMobile = ref(false)
// // Check screen size
const checkIsMobile = () => { const checkIsMobile = () => {
isMobile.value = window.innerWidth < 768 isMobile.value = window.innerWidth < 768
if (isMobile.value) { if (isMobile.value) {
@ -242,29 +240,29 @@ const checkIsMobile = () => {
} }
} }
// // Toggle sidebar
const toggleSidebar = () => { const toggleSidebar = () => {
collapsed.value = !collapsed.value collapsed.value = !collapsed.value
} }
// // Sidebar collapse event
const onSidebarCollapse = () => { const onSidebarCollapse = () => {
collapsed.value = true collapsed.value = true
} }
// // Sidebar expand event
const onSidebarExpand = () => { const onSidebarExpand = () => {
collapsed.value = false collapsed.value = false
} }
// - // Menu select event - auto close on mobile
const onMenuSelect = () => { const onMenuSelect = () => {
if (isMobile.value) { if (isMobile.value) {
collapsed.value = true collapsed.value = true
} }
} }
// // Handle window resize
const handleWindowResize = () => { const handleWindowResize = () => {
checkIsMobile() checkIsMobile()
adjustContainerSize() adjustContainerSize()
@ -289,13 +287,13 @@ interface HistoryItem {
} }
const fileInputRef = ref<HTMLInputElement | null>(null) 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
const urlInputRef = ref<HTMLInputElement | null>(null) const urlInputRef = ref<HTMLInputElement | null>(null)
const uploadedImage = ref<File | null>(null) const uploadedImage = ref<File | null>(null)
const uploadedImageUrl = ref<string>('') const uploadedImageUrl = ref<string>('')
const resultImage = 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 imageUrl = ref<string>('')
const resultImageUrl = computed(() => { const resultImageUrl = computed(() => {
if (!resultImage.value) return '' if (!resultImage.value) return ''
@ -309,57 +307,57 @@ const dragCounter = ref(0)
const processing = ref(false) const processing = ref(false)
const splitPosition = ref(0) const splitPosition = ref(0)
// - 使 // Sample images for quick experience - suitable for background removal
const sampleImages = [ const sampleImages = [
{ {
id: 'sample-1', id: 'sample-1',
url: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=1024&h=1024&fit=crop&q=80',
name: '人物示例' name: t('Portrait Sample')
}, },
{ {
id: 'sample-2', id: 'sample-2',
url: 'https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?w=1024&h=1024&fit=crop&q=80',
name: '人物示例' name: t('Portrait Sample')
}, },
{ {
id: 'sample-3', id: 'sample-3',
url: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=1024&h=1024&fit=crop&q=80',
name: '产品示例' name: t('Product Sample')
}, },
{ {
id: 'sample-4', id: 'sample-4',
url: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=1024&h=1024&fit=crop&q=80',
name: '产品示例' name: t('Product Sample')
}, },
{ {
id: 'sample-5', id: 'sample-5',
url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1024&h=1024&fit=crop&q=80',
name: '动物示例' name: t('Animal Sample')
}, },
{ {
id: 'sample-6', id: 'sample-6',
url: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e?w=1024&h=1024&fit=crop&q=80',
name: '物品示例' name: t('Object Sample')
}, },
{ {
id: 'sample-7', id: 'sample-7',
url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=1024&h=1024&fit=crop&q=80',
name: '人物示例' name: t('Portrait Sample')
}, },
{ {
id: 'sample-8', id: 'sample-8',
url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=1024&h=1024&fit=crop&q=80',
name: '人物示例' name: t('Portrait Sample')
}, },
{ {
id: 'sample-9', id: 'sample-9',
url: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=1024&h=1024&fit=crop&q=80', url: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=1024&h=1024&fit=crop&q=80',
name: '人物示例' name: t('Portrait Sample')
}, },
{ {
id: 'sample-10', id: 'sample-10',
url: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=1024&h=1024&fit=crop&q=80', 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) const comparisonContainerRef = ref<HTMLElement | null>(null)
@ -453,12 +451,11 @@ const processFile = async (file: File) => {
} }
reader.readAsDataURL(compressedFile) reader.readAsDataURL(compressedFile)
} catch (error) { } 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) => { const handleSampleImageClick = async (imageUrl: string) => {
if (processing.value) return if (processing.value) return
@ -472,7 +469,7 @@ const handleSampleImageClick = async (imageUrl: string) => {
const blob = await response.blob() const blob = await response.blob()
// // Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'] const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
if (!validTypes.includes(blob.type)) { if (!validTypes.includes(blob.type)) {
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP')) message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
@ -480,7 +477,7 @@ const handleSampleImageClick = async (imageUrl: string) => {
return return
} }
// // Validate file size
const maxSize = 10 * 1024 * 1024 const maxSize = 10 * 1024 * 1024
if (blob.size > maxSize) { if (blob.size > maxSize) {
message.warning(t('Image size exceeds 10MB limit')) message.warning(t('Image size exceeds 10MB limit'))
@ -488,11 +485,10 @@ const handleSampleImageClick = async (imageUrl: string) => {
return return
} }
// File // Convert to File object
const file = new File([blob], 'sample-image.jpg', { type: blob.type }) const file = new File([blob], 'sample-image.jpg', { type: blob.type })
await processFile(file) await processFile(file)
} catch (error: any) { } catch (error: any) {
console.error('加载示例图片失败:', error)
let errorMessage = t('Failed to load sample image') let errorMessage = t('Failed to load sample image')
if (error.message?.includes('CORS')) { if (error.message?.includes('CORS')) {
@ -515,7 +511,7 @@ const handleRemoveBackground = async () => {
processing.value = true processing.value = true
resultImage.value = '' resultImage.value = ''
// 使 // Helper function to handle successful result (internal use)
const handleSuccess = async (imageUrl: string): Promise<void> => { const handleSuccess = async (imageUrl: string): Promise<void> => {
resultImage.value = imageUrl resultImage.value = imageUrl
await cacheResultImage(imageUrl) await cacheResultImage(imageUrl)
@ -581,13 +577,13 @@ const handleRemoveBackground = async () => {
message.error(result.error) message.error(result.error)
} }
} catch (parseError) { } catch (parseError) {
console.error('Failed to parse JSON:', parseError, 'Line:', line) // Failed to parse JSON, continue processing
} }
} }
} }
} }
// // Process last line
if (buffer.trim()) { if (buffer.trim()) {
try { try {
const result = JSON.parse(buffer.trim()) const result = JSON.parse(buffer.trim())
@ -598,7 +594,6 @@ const handleRemoveBackground = async () => {
message.error(result.error || t('Failed to remove background')) message.error(result.error || t('Failed to remove background'))
} }
} catch (parseError) { } catch (parseError) {
console.error('Failed to parse final JSON:', parseError)
message.error(t('Failed to parse response')) message.error(t('Failed to parse response'))
} }
} else { } else {
@ -620,17 +615,17 @@ const handleRemoveBackground = async () => {
} }
/** /**
* 缓存结果图片为 blob URL用于下载 * Cache result image as blob URL for download
*/ */
const cacheResultImage = async (imageUrl: string) => { const cacheResultImage = async (imageUrl: string) => {
try { try {
// blob URL // Clean up old blob URL
if (resultImageBlobUrl.value) { if (resultImageBlobUrl.value) {
URL.revokeObjectURL(resultImageBlobUrl.value) URL.revokeObjectURL(resultImageBlobUrl.value)
resultImageBlobUrl.value = '' resultImageBlobUrl.value = ''
} }
// blob URL // Fetch image and convert to blob URL
const response = await fetch(imageUrl) const response = await fetch(imageUrl)
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status}`) throw new Error(`Failed to fetch image: ${response.status}`)
@ -638,15 +633,14 @@ const cacheResultImage = async (imageUrl: string) => {
const blob = await response.blob() const blob = await response.blob()
resultImageBlobUrl.value = URL.createObjectURL(blob) resultImageBlobUrl.value = URL.createObjectURL(blob)
} catch (error) { } catch (error) {
console.error('缓存图片失败:', error) // Cache failure doesn't affect display, only download needs to refetch
//
} }
} }
const handleDownload = async () => { const handleDownload = async () => {
if (!resultImage.value) return if (!resultImage.value) return
// // Check login status
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
message.warning(t('Please login to download')) message.warning(t('Please login to download'))
showLoginModal.value = true showLoginModal.value = true
@ -654,10 +648,10 @@ const handleDownload = async () => {
} }
try { try {
// 使 blob URL // Prefer cached blob URL
let blobUrl = resultImageBlobUrl.value let blobUrl = resultImageBlobUrl.value
// // If no cache, fetch and cache
if (!blobUrl) { if (!blobUrl) {
const response = await fetch(resultImage.value) const response = await fetch(resultImage.value)
if (!response.ok) { if (!response.ok) {
@ -672,9 +666,8 @@ const handleDownload = async () => {
link.href = blobUrl link.href = blobUrl
link.download = `removed-background-${Date.now()}.png` link.download = `removed-background-${Date.now()}.png`
link.click() link.click()
// blob URL使 // Don't immediately clean up blob URL, keep cache for subsequent downloads
} catch (error) { } catch (error) {
console.error('下载失败:', error)
message.error(t('Failed to download image')) message.error(t('Failed to download image'))
} }
} }
@ -683,7 +676,7 @@ const resetUpload = () => {
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) { if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(uploadedImageUrl.value) URL.revokeObjectURL(uploadedImageUrl.value)
} }
// blob URL // Clean up result image blob URL cache
if (resultImageBlobUrl.value) { if (resultImageBlobUrl.value) {
URL.revokeObjectURL(resultImageBlobUrl.value) URL.revokeObjectURL(resultImageBlobUrl.value)
resultImageBlobUrl.value = '' resultImageBlobUrl.value = ''
@ -709,10 +702,10 @@ const resetUpload = () => {
const adjustContainerSize = async () => { const adjustContainerSize = async () => {
await nextTick() await nextTick()
// DOM // Wait for multiple render cycles to ensure DOM is fully updated
await new Promise(resolve => setTimeout(resolve, 0)) await new Promise(resolve => setTimeout(resolve, 0))
// // Handle comparison view container
const container = comparisonContainerRef.value const container = comparisonContainerRef.value
if (container) { if (container) {
const img = originalImageRef.value || resultImageRef.value const img = originalImageRef.value || resultImageRef.value
@ -746,7 +739,7 @@ const adjustContainerSize = async () => {
} }
} }
// // Handle single image view container
const singleWrapper = singleImageWrapperRef.value const singleWrapper = singleImageWrapperRef.value
if (singleWrapper) { if (singleWrapper) {
const img = singleImageRef.value const img = singleImageRef.value
@ -906,13 +899,13 @@ const handleUrlSubmit = async () => {
const originalFile = new File([blob], 'image-from-url', { type: blob.type }) const originalFile = new File([blob], 'image-from-url', { type: blob.type })
// 1024x1024 // Compress image to 1024x1024
try { try {
const compressedFile = await compressImageFile(originalFile, { const compressedFile = await compressImageFile(originalFile, {
maxWidth: 1024, maxWidth: 1024,
maxHeight: 1024, maxHeight: 1024,
quality: 0.92, quality: 0.92,
mode: 'contain' // mode: 'contain' // Maintain aspect ratio
}) })
uploadedImage.value = compressedFile uploadedImage.value = compressedFile
@ -920,12 +913,10 @@ const handleUrlSubmit = async () => {
await handleRemoveBackground() await handleRemoveBackground()
} catch (error) { } catch (error) {
console.error('图片压缩失败:', error)
message.error(t('Image processing failed, please try again')) message.error(t('Image processing failed, please try again'))
processing.value = false processing.value = false
} }
} catch (error: any) { } catch (error: any) {
console.error('加载图片URL失败:', error)
let errorMessage = t('Failed to load image from URL') let errorMessage = t('Failed to load image from URL')
if (error.message?.includes('CORS')) { if (error.message?.includes('CORS')) {
@ -949,7 +940,7 @@ const selectHistoryItem = async (index: number) => {
splitPosition.value = 0 splitPosition.value = 0
uploadedImage.value = item.originalImageFile uploadedImage.value = item.originalImageFile
// // Cache result image from history
if (item.resultImage) { if (item.resultImage) {
await cacheResultImage(item.resultImage) await cacheResultImage(item.resultImage)
} }
@ -986,11 +977,11 @@ onMounted(async () => {
window.addEventListener('resize', handleWindowResize) window.addEventListener('resize', handleWindowResize)
window.addEventListener('paste', handlePaste) window.addEventListener('paste', handlePaste)
// // Initialize auth state
await authStore.initAuth() await authStore.initAuth()
// // Detect mobile
checkIsMobile() checkIsMobile()
}) })
@ -1000,7 +991,7 @@ onUnmounted(() => {
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) { if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(uploadedImageUrl.value) URL.revokeObjectURL(uploadedImageUrl.value)
} }
// blob URL // Clean up result image blob URL cache
if (resultImageBlobUrl.value) { if (resultImageBlobUrl.value) {
URL.revokeObjectURL(resultImageBlobUrl.value) URL.revokeObjectURL(resultImageBlobUrl.value)
} }
@ -1009,7 +1000,7 @@ onUnmounted(() => {
<template> <template>
<div class="home-page" @dragenter.prevent="handleDragEnter" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop"> <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"> <template v-if="!isLoggedIn">
<!-- Header --> <!-- Header -->
<header class="marketing-header"> <header class="marketing-header">
@ -1159,7 +1150,7 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 示例图片区块 --> <!-- Sample images section -->
<div v-if="!uploadedImage" class="sample-images-section"> <div v-if="!uploadedImage" class="sample-images-section">
<p class="sample-images-title">{{ t('Click image to try') }}</p> <p class="sample-images-title">{{ t('Click image to try') }}</p>
<div class="sample-images-container"> <div class="sample-images-container">
@ -1188,7 +1179,7 @@ onUnmounted(() => {
<footer class="marketing-footer"> <footer class="marketing-footer">
<div class="footer-container"> <div class="footer-container">
<div class="footer-content"> <div class="footer-content">
<!-- 左侧Logo 和社交媒体 --> <!-- Left: Logo and social media -->
<div class="footer-left"> <div class="footer-left">
<div class="footer-logo"> <div class="footer-logo">
<router-link to="/" class="logo-link"> <router-link to="/" class="logo-link">
@ -1212,7 +1203,7 @@ onUnmounted(() => {
<Icon icon="ant-design:zhihu-square-filled" /> <Icon icon="ant-design:zhihu-square-filled" />
</a> </a>
</template> </template>
<!-- 英文版社交图标 --> <!-- English version social icons -->
<template v-else> <template v-else>
<a href="#" class="social-icon" title="Twitter"> <a href="#" class="social-icon" title="Twitter">
<Icon icon="tabler:brand-twitter" /> <Icon icon="tabler:brand-twitter" />
@ -1236,7 +1227,7 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 右侧三列链接 --> <!-- Right: three column links -->
<div class="footer-right"> <div class="footer-right">
<div class="footer-column"> <div class="footer-column">
<h3 class="footer-title">{{ t('Products & Services') }}</h3> <h3 class="footer-title">{{ t('Products & Services') }}</h3>
@ -1269,10 +1260,10 @@ onUnmounted(() => {
</footer> </footer>
</template> </template>
<!-- 已登录状态显示应用布局 sidebar header --> <!-- Logged in: show app layout (with sidebar and header) -->
<template v-else> <template v-else>
<n-layout has-sider class="app-layout"> <n-layout has-sider class="app-layout">
<!-- 侧边栏 --> <!-- Sidebar -->
<n-layout-sider <n-layout-sider
bordered bordered
collapse-mode="width" collapse-mode="width"
@ -1288,14 +1279,14 @@ onUnmounted(() => {
<AppSidebar :collapsed="collapsed" @menu-select="onMenuSelect" /> <AppSidebar :collapsed="collapsed" @menu-select="onMenuSelect" />
</n-layout-sider> </n-layout-sider>
<!-- 主内容区 --> <!-- Main content area -->
<n-layout> <n-layout>
<!-- 顶部导航 --> <!-- Top navigation -->
<n-layout-header bordered> <n-layout-header bordered>
<AppHeader @toggle-sidebar="toggleSidebar" /> <AppHeader @toggle-sidebar="toggleSidebar" />
</n-layout-header> </n-layout-header>
<!-- 内容区域 --> <!-- Content area -->
<n-layout-content> <n-layout-content>
<div class="content-wrapper"> <div class="content-wrapper">
<div v-if="isDragging" class="global-drag-overlay"> <div v-if="isDragging" class="global-drag-overlay">
@ -1426,7 +1417,7 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 示例图片区块 --> <!-- Sample images section -->
<div v-if="!uploadedImage" class="sample-images-section"> <div v-if="!uploadedImage" class="sample-images-section">
<p class="sample-images-title">{{ t('Click image to try') }}</p> <p class="sample-images-title">{{ t('Click image to try') }}</p>
<div class="sample-images-container"> <div class="sample-images-container">
@ -1453,7 +1444,7 @@ onUnmounted(() => {
</n-layout-content> </n-layout-content>
</n-layout> </n-layout>
<!-- 移动端遮罩层 --> <!-- Mobile overlay -->
<div <div
v-if="isMobile && !collapsed" v-if="isMobile && !collapsed"
class="mobile-overlay" class="mobile-overlay"
@ -1462,7 +1453,7 @@ onUnmounted(() => {
</n-layout> </n-layout>
</template> </template>
<!-- 登录弹窗 --> <!-- Login modal -->
<n-modal <n-modal
v-model:show="showLoginModal" v-model:show="showLoginModal"
preset="card" preset="card"
@ -1531,7 +1522,7 @@ onUnmounted(() => {
</div> </div>
</n-modal> </n-modal>
<!-- 注册弹窗 --> <!-- Signup modal -->
<n-modal <n-modal
v-model:show="showSignupModal" v-model:show="showSignupModal"
preset="card" preset="card"
@ -1642,7 +1633,7 @@ onUnmounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.home-page { .home-page {
min-height: 100vh; /* 使用 min-height 而不是固定 height */ min-height: 100vh; /* Use min-height instead of fixed height */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: white; background: white;
@ -1650,7 +1641,7 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
} }
/* 应用布局样式(登录后) */ /* App layout styles (after login) */
.app-layout { .app-layout {
height: 100vh; height: 100vh;
} }
@ -1661,14 +1652,14 @@ onUnmounted(() => {
overflow-y: auto; overflow-y: auto;
} }
/* 使用Naive UI内置的sticky功能 */ /* Use Naive UI built-in sticky functionality */
:deep(.n-layout-header) { :deep(.n-layout-header) {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1000; z-index: 1000;
} }
/* 移动端遮罩层 */ /* Mobile overlay */
.mobile-overlay { .mobile-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -1679,9 +1670,9 @@ onUnmounted(() => {
z-index: 999; z-index: 999;
} }
/* 移动端样式 */ /* Mobile styles */
@media (max-width: 767px) { @media (max-width: 767px) {
/* 移动端时完全隐藏侧边栏 */ /* Completely hide sidebar on mobile */
:deep(.n-layout-sider) { :deep(.n-layout-sider) {
position: fixed !important; position: fixed !important;
top: 0; top: 0;
@ -1694,18 +1685,18 @@ onUnmounted(() => {
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
/* 移动端侧边栏打开时的样式 */ /* Mobile sidebar open styles */
:deep(.n-layout-sider:not(.n-layout-sider--collapsed)) { :deep(.n-layout-sider:not(.n-layout-sider--collapsed)) {
transform: translateX(0) !important; transform: translateX(0) !important;
} }
/* 移动端主内容区域占满全宽 */ /* Mobile main content area takes full width */
:deep(.n-layout) { :deep(.n-layout) {
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
/* 桌面端保持原有样式 */ /* Desktop maintains original styles */
@media (min-width: 768px) { @media (min-width: 768px) {
:deep(.n-layout-sider) { :deep(.n-layout-sider) {
position: relative !important; position: relative !important;
@ -1967,7 +1958,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-height: 600px; /* 设置最小高度确保容器足够大 */ min-height: 600px; /* Set minimum height to ensure container is large enough */
padding: 12px 16px; padding: 12px 16px;
} }
@ -1976,10 +1967,10 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-height: 600px; /* 设置最小高度确保容器足够大 */ min-height: 600px; /* Set minimum height to ensure container is large enough */
} }
/* 示例图片区块 */ /* Sample images section */
.sample-images-section { .sample-images-section {
margin-top: 24px; margin-top: 24px;
background: white; background: white;
@ -2099,7 +2090,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-height: 600px; /* 设置最小高度 */ min-height: 600px; /* Set minimum height */
background: white; background: white;
border-radius: 12px; border-radius: 12px;
padding: 24px; padding: 24px;
@ -2113,7 +2104,7 @@ onUnmounted(() => {
border: none; border: none;
box-shadow: none; box-shadow: none;
background: transparent; background: transparent;
min-height: 600px; /* 确保预览区域有足够高度 */ min-height: 600px; /* Ensure preview area has sufficient height */
height: auto; height: auto;
} }

View File

@ -51,10 +51,10 @@
</div> </div>
</n-card> </n-card>
<!-- 功能设置卡片 --> <!-- Feature settings card -->
<n-card> <n-card>
<n-list> <n-list>
<!-- 应用市场开发者 --> <!-- Marketplace developer -->
<n-list-item v-if="!teamInfo?.is_developer"> <n-list-item v-if="!teamInfo?.is_developer">
<n-thing> <n-thing>
<template #header> <template #header>
@ -73,7 +73,7 @@
</n-thing> </n-thing>
</n-list-item> </n-list-item>
<!-- 双因素认证 --> <!-- Two-factor authentication -->
<n-list-item> <n-list-item>
<n-thing> <n-thing>
<template #header> <template #header>
@ -90,7 +90,7 @@
</n-thing> </n-thing>
</n-list-item> </n-list-item>
<!-- 重置密码 --> <!-- Reset password -->
<n-list-item> <n-list-item>
<n-thing> <n-thing>
<template #header> <template #header>
@ -110,7 +110,7 @@
</n-list> </n-list>
</n-card> </n-card>
<!-- 推荐有礼 --> <!-- Referral program -->
<n-card v-if="referralLink"> <n-card v-if="referralLink">
<n-list> <n-list>
<n-list-item> <n-list-item>
@ -137,7 +137,7 @@
</div> </div>
</n-card> </n-card>
<!-- Jingrow 合作伙伴 --> <!-- Jingrow Partner -->
<n-card v-if="!teamInfo?.jerp_partner"> <n-card v-if="!teamInfo?.jerp_partner">
<n-list> <n-list>
<n-list-item> <n-list-item>
@ -192,10 +192,10 @@
</n-space> </n-space>
</n-tab-pane> </n-tab-pane>
<!-- 开发者标签页 --> <!-- Developer tab -->
<n-tab-pane name="developer" :tab="t('Developer')"> <n-tab-pane name="developer" :tab="t('Developer')">
<n-space vertical :size="24"> <n-space vertical :size="24">
<!-- API 访问 --> <!-- API Access -->
<n-card :title="t('API Access')"> <n-card :title="t('API Access')">
<n-space vertical :size="20"> <n-space vertical :size="20">
<div class="api-description"> <div class="api-description">
@ -215,7 +215,7 @@
</n-space> </n-space>
</n-card> </n-card>
<!-- SSH 密钥 --> <!-- SSH Keys -->
<n-card :title="t('SSH Keys')"> <n-card :title="t('SSH Keys')">
<n-space vertical :size="16"> <n-space vertical :size="16">
<div class="ssh-actions"> <div class="ssh-actions">
@ -237,7 +237,7 @@
</n-space> </n-space>
</n-card> </n-card>
<!-- 功能标志仅管理员可见 --> <!-- Feature flags (admin only) -->
<n-card v-if="isAdmin" :title="t('Advanced Features')"> <n-card v-if="isAdmin" :title="t('Advanced Features')">
<n-form :model="featureFlags" label-placement="left" label-width="200px"> <n-form :model="featureFlags" label-placement="left" label-width="200px">
<n-form-item <n-form-item
@ -268,10 +268,10 @@
</n-space> </n-space>
</n-tab-pane> </n-tab-pane>
<!-- 系统设置标签页 --> <!-- System settings tab -->
<n-tab-pane name="system" :tab="t('System Settings')"> <n-tab-pane name="system" :tab="t('System Settings')">
<n-grid :cols="2" :x-gap="24" :y-gap="24"> <n-grid :cols="2" :x-gap="24" :y-gap="24">
<!-- 左栏系统设置 --> <!-- Left column: System settings -->
<n-grid-item> <n-grid-item>
<n-card :title="t('System Settings')"> <n-card :title="t('System Settings')">
<n-form :model="systemSettings" label-placement="left" label-width="120px"> <n-form :model="systemSettings" label-placement="left" label-width="120px">
@ -320,7 +320,7 @@
</n-card> </n-card>
</n-grid-item> </n-grid-item>
<!-- 右栏环境配置仅系统管理员可见 --> <!-- Right column: Environment configuration (system admin only) -->
<n-grid-item v-if="isAdmin"> <n-grid-item v-if="isAdmin">
<n-card :title="t('Environment Configuration')"> <n-card :title="t('Environment Configuration')">
<n-alert type="warning" style="margin-bottom: 16px"> <n-alert type="warning" style="margin-bottom: 16px">
@ -455,7 +455,7 @@
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
<!-- 编辑个人资料对话框 --> <!-- Edit profile dialog -->
<n-modal v-model:show="showProfileEditDialog" preset="card" :title="t('Update Profile Information')" :style="{ width: '600px' }"> <n-modal v-model:show="showProfileEditDialog" preset="card" :title="t('Update Profile Information')" :style="{ width: '600px' }">
<n-form :model="profileForm" label-placement="top"> <n-form :model="profileForm" label-placement="top">
<n-space vertical :size="20"> <n-space vertical :size="20">
@ -480,7 +480,7 @@
</template> </template>
</n-modal> </n-modal>
<!-- 创建 API Secret 对话框 --> <!-- Create API Secret dialog -->
<n-modal v-model:show="showCreateSecretDialog" preset="card" :title="t('API Access')" :style="{ width: '700px' }"> <n-modal v-model:show="showCreateSecretDialog" preset="card" :title="t('API Access')" :style="{ width: '700px' }">
<n-space vertical :size="20"> <n-space vertical :size="20">
<div v-if="!createSecretData"> <div v-if="!createSecretData">
@ -515,7 +515,7 @@
</template> </template>
</n-modal> </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-modal v-model:show="showAddSSHKeyDialog" preset="card" :title="t('Add New SSH Key')" :style="{ width: '700px' }">
<n-space vertical :size="20"> <n-space vertical :size="20">
<p class="text-base">{{ t('Add a new SSH key to your account') }}</p> <p class="text-base">{{ t('Add a new SSH key to your account') }}</p>
@ -543,11 +543,11 @@
</template> </template>
</n-modal> </n-modal>
<!-- 双因素认证对话框 --> <!-- Two-factor authentication dialog -->
<n-modal v-model:show="show2FADialog" preset="card" :title="twoFactorAuthTitle" :style="{ width: '700px' }"> <n-modal v-model:show="show2FADialog" preset="card" :title="twoFactorAuthTitle" :style="{ width: '700px' }">
<n-spin :show="loadingQRCode"> <n-spin :show="loadingQRCode">
<n-space vertical :size="24"> <n-space vertical :size="24">
<!-- 禁用 2FA 模式 --> <!-- Disable 2FA mode -->
<div v-if="is2FAEnabled"> <div v-if="is2FAEnabled">
<n-alert <n-alert
type="error" type="error"
@ -573,7 +573,7 @@
<!-- 启用 2FA 模式 --> <!-- 启用 2FA 模式 -->
<div v-else> <div v-else>
<!-- QR - 居中显示 --> <!-- QR code - centered display -->
<div class="tfa-qr-container" v-if="qrCodeUrl"> <div class="tfa-qr-container" v-if="qrCodeUrl">
<img <img
:src="`https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${encodeURIComponent(qrCodeUrl)}`" :src="`https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${encodeURIComponent(qrCodeUrl)}`"
@ -582,7 +582,7 @@
/> />
</div> </div>
<!-- 步骤说明 --> <!-- Step instructions -->
<n-card> <n-card>
<h3 class="text-lg font-semibold mb-3">{{ t('Steps to Enable Two-Factor Authentication') }}</h3> <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"> <ol class="ml-4 list-decimal space-y-2 text-sm text-gray-700 mb-4">
@ -606,7 +606,7 @@
</p> </p>
</n-card> </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-form-item :label="t('Verify the code in the app to enable two-factor authentication')" class="mt-6">
<n-input <n-input
v-model:value="totpCode" v-model:value="totpCode"
@ -617,14 +617,14 @@
</n-form-item> </n-form-item>
</div> </div>
<!-- 错误提示 --> <!-- Error message -->
<n-alert <n-alert
v-if="twoFAError" v-if="twoFAError"
type="error" type="error"
:title="twoFAError" :title="twoFAError"
/> />
<!-- 操作按钮 --> <!-- Action buttons -->
<n-button <n-button
v-if="!is2FAEnabled" v-if="!is2FAEnabled"
type="primary" type="primary"
@ -656,7 +656,7 @@
</template> </template>
</n-modal> </n-modal>
<!-- 重置密码对话框 --> <!-- Reset password dialog -->
<n-modal v-model:show="showResetPasswordDialog" preset="card" :title="t('Reset Password')" :style="{ width: '700px' }"> <n-modal v-model:show="showResetPasswordDialog" preset="card" :title="t('Reset Password')" :style="{ width: '700px' }">
<n-space vertical :size="20"> <n-space vertical :size="20">
<n-form-item :label="t('Current Password')"> <n-form-item :label="t('Current Password')">
@ -719,7 +719,7 @@
</template> </template>
</n-modal> </n-modal>
<!-- 添加合作伙伴代码对话框 --> <!-- Add partner code dialog -->
<n-modal v-model:show="showAddPartnerCodeDialog" preset="card" :title="t('Link Partner Account')" :style="{ width: '700px' }"> <n-modal v-model:show="showAddPartnerCodeDialog" preset="card" :title="t('Link Partner Account')" :style="{ width: '700px' }">
<n-space vertical :size="20"> <n-space vertical :size="20">
<p class="text-base">{{ t('Enter the partner code provided by your partner') }}</p> <p class="text-base">{{ t('Enter the partner code provided by your partner') }}</p>
@ -756,7 +756,7 @@
</template> </template>
</n-modal> </n-modal>
<!-- 移除合作伙伴对话框 --> <!-- Remove partner dialog -->
<n-modal <n-modal
v-model:show="showRemovePartnerDialog" v-model:show="showRemovePartnerDialog"
preset="dialog" preset="dialog"
@ -838,25 +838,25 @@ const message = useMessage()
const dialog = useDialog() const dialog = useDialog()
const authStore = useAuthStore() const authStore = useAuthStore()
// // Currently active tab
const activeTab = ref('profile') const activeTab = ref('profile')
// // Check if system administrator
const isAdmin = computed(() => { const isAdmin = computed(() => {
const user = authStore.user const user = authStore.user
return user?.user === 'Administrator' || user?.user_type === 'System User' return user?.user === 'Administrator' || user?.user_type === 'System User'
}) })
// local // Check if local run mode
const isLocalMode = computed(() => { const isLocalMode = computed(() => {
return envConfig.run_mode === 'local' return envConfig.run_mode === 'local'
}) })
// // User account information
const userAccountInfo = ref<any>(null) const userAccountInfo = ref<any>(null)
const userAccountInfoLoading = ref(false) const userAccountInfoLoading = ref(false)
// // Profile related
const showProfileEditDialog = ref(false) const showProfileEditDialog = ref(false)
const profileForm = reactive({ const profileForm = reactive({
first_name: '', first_name: '',
@ -867,10 +867,10 @@ const profileForm = reactive({
}) })
const profileSaving = ref(false) const profileSaving = ref(false)
// Team // Team information
const teamInfo = ref<any>(null) const teamInfo = ref<any>(null)
// // Referral program
const referralLink = computed(() => { const referralLink = computed(() => {
if (teamInfo.value?.referrer_id) { if (teamInfo.value?.referrer_id) {
return `${location.origin}/dashboard/signup?referrer=${teamInfo.value.referrer_id}` return `${location.origin}/dashboard/signup?referrer=${teamInfo.value.referrer_id}`
@ -878,7 +878,7 @@ const referralLink = computed(() => {
return '' return ''
}) })
// // Partner
const showAddPartnerCodeDialog = ref(false) const showAddPartnerCodeDialog = ref(false)
const showRemovePartnerDialog = ref(false) const showRemovePartnerDialog = ref(false)
const partnerCode = ref('') const partnerCode = ref('')
@ -888,7 +888,7 @@ const partnerName = ref('')
const partnerCodeError = ref('') const partnerCodeError = ref('')
const addPartnerCodeLoading = ref(false) const addPartnerCodeLoading = ref(false)
// // Reset password
const oldPassword = ref('') const oldPassword = ref('')
const newPassword = ref('') const newPassword = ref('')
const confirmPassword = ref('') const confirmPassword = ref('')
@ -899,7 +899,7 @@ const passwordError = ref('')
const resetPasswordLoading = ref(false) const resetPasswordLoading = ref(false)
const passwordStrengthTimeout = ref<any>(null) const passwordStrengthTimeout = ref<any>(null)
// // Two-factor authentication
const show2FADialog = ref(false) const show2FADialog = ref(false)
const qrCodeUrl = ref('') const qrCodeUrl = ref('')
const totpCode = ref('') const totpCode = ref('')
@ -927,10 +927,10 @@ const loading2FA = ref(false)
const loadingQRCode = ref(false) const loadingQRCode = ref(false)
const twoFAError = ref('') const twoFAError = ref('')
// // Reset password
const showResetPasswordDialog = ref(false) const showResetPasswordDialog = ref(false)
// API Secret // API Secret related
const showCreateSecretDialog = ref(false) const showCreateSecretDialog = ref(false)
const createSecretData = ref<{ api_key: string; api_secret: string } | null>(null) const createSecretData = ref<{ api_key: string; api_secret: string } | null>(null)
const createSecretLoading = ref(false) 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') return userAccountInfo.value?.api_key ? t('Regenerate API Key') : t('Create New API Key')
}) })
// SSH // SSH key related
const sshKeys = ref<any[]>([]) const sshKeys = ref<any[]>([])
const sshKeysLoading = ref(false) const sshKeysLoading = ref(false)
const showAddSSHKeyDialog = 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'") 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 = [ const sshKeyColumns = [
{ {
title: t('SSH Fingerprint'), title: t('SSH Fingerprint'),
@ -984,7 +984,7 @@ const sshKeyColumns = [
} }
] ]
// // Feature flags related
const featureFlags = reactive<Record<string, boolean>>({}) const featureFlags = reactive<Record<string, boolean>>({})
const featureFlagsSaving = ref(false) const featureFlagsSaving = ref(false)
const featureFlagFields = [ const featureFlagFields = [
@ -992,11 +992,11 @@ const featureFlagFields = [
{ label: t('Enable security portal'), fieldname: 'security_portal_enabled' } { label: t('Enable security portal'), fieldname: 'security_portal_enabled' }
] ]
const featureFlagsDirty = computed(() => { const featureFlagsDirty = computed(() => {
// // Need to check if there are changes based on actual data
return false return false
}) })
// // Environment configuration
const envConfig = reactive<Partial<EnvironmentConfig>>({}) const envConfig = reactive<Partial<EnvironmentConfig>>({})
const envConfigLoading = ref(false) const envConfigLoading = ref(false)
const envConfigSaving = ref(false) const envConfigSaving = ref(false)
@ -1009,13 +1009,13 @@ const systemSettings = reactive({
timezone: getCurrentTimezone() timezone: getCurrentTimezone()
}) })
// // Language options
const languageOptions = locales.map(locale => ({ const languageOptions = locales.map(locale => ({
label: `${locale.flag} ${locale.name}`, label: `${locale.flag} ${locale.name}`,
value: locale.code value: locale.code
})) }))
// // Items per page options
const pageSizeOptions = [ const pageSizeOptions = [
{ label: '10', value: 10 }, { label: '10', value: 10 },
{ label: '20', value: 20 }, { label: '20', value: 20 },
@ -1023,30 +1023,30 @@ const pageSizeOptions = [
{ label: '100', value: 100 } { 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 timezoneOptions = ref<Array<{ type: string; label: string; key: string; children: Array<{ label: string; value: string }> }>>([])
const timezoneError = ref<string | null>(null) const timezoneError = ref<string | null>(null)
// // Database type options
const dbTypeOptions = [ const dbTypeOptions = [
{ label: 'MariaDB', value: 'mariadb' }, { label: 'MariaDB', value: 'mariadb' },
{ label: 'MySQL', value: 'mysql' }, { label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'postgresql' } { label: 'PostgreSQL', value: 'postgresql' }
] ]
// // Run mode options
const runModeOptions = [ const runModeOptions = [
{ label: 'API', value: 'api' }, { label: 'API', value: 'api' },
{ label: 'Local', value: 'local' } { label: 'Local', value: 'local' }
] ]
// // Environment options
const environmentOptions = [ const environmentOptions = [
{ label: 'Development', value: 'development' }, { label: 'Development', value: 'development' },
{ label: 'Production', value: 'production' } { label: 'Production', value: 'production' }
] ]
// // Log level options
const logLevelOptions = [ const logLevelOptions = [
{ label: 'DEBUG', value: 'DEBUG' }, { label: 'DEBUG', value: 'DEBUG' },
{ label: 'INFO', value: 'INFO' }, { label: 'INFO', value: 'INFO' },
@ -1061,21 +1061,21 @@ const changeLanguage = (locale: string) => {
} }
const saveSystemSettings = () => { const saveSystemSettings = () => {
// // Save app name
localStorage.setItem('appName', systemSettings.appName) localStorage.setItem('appName', systemSettings.appName)
// // Save items per page setting
localStorage.setItem('itemsPerPage', systemSettings.itemsPerPage.toString()) localStorage.setItem('itemsPerPage', systemSettings.itemsPerPage.toString())
// // Save timezone setting
localStorage.setItem('timezone', systemSettings.timezone) localStorage.setItem('timezone', systemSettings.timezone)
message.success(t('System settings saved')) message.success(t('System settings saved'))
// // Auto refresh page after saving to apply new settings
setTimeout(() => { setTimeout(() => {
window.location.reload() window.location.reload()
}, 1000) }, 1000)
} }
// // Load environment configuration
const loadEnvironmentConfig = async (showMessage = true) => { const loadEnvironmentConfig = async (showMessage = true) => {
if (!isAdmin.value) { if (!isAdmin.value) {
return return
@ -1103,7 +1103,7 @@ const loadEnvironmentConfig = async (showMessage = true) => {
} }
} }
// // Save environment configuration
const saveEnvironmentConfig = async () => { const saveEnvironmentConfig = async () => {
if (!isAdmin.value) { if (!isAdmin.value) {
message.error(t('Only system administrators can edit environment configuration')) message.error(t('Only system administrators can edit environment configuration'))
@ -1115,7 +1115,7 @@ const saveEnvironmentConfig = async () => {
const result = await updateEnvironmentConfig(envConfig) const result = await updateEnvironmentConfig(envConfig)
if (result.success) { if (result.success) {
message.success(result.message || t('Environment configuration saved')) message.success(result.message || t('Environment configuration saved'))
// // Reload configuration to get latest values (silent load, no message)
await loadEnvironmentConfig(false) await loadEnvironmentConfig(false)
} else { } else {
message.error(result.message || t('Failed to save environment configuration')) message.error(result.message || t('Failed to save environment configuration'))
@ -1127,7 +1127,7 @@ const saveEnvironmentConfig = async () => {
} }
} }
// // Restart environment
const handleRestartEnvironment = () => { const handleRestartEnvironment = () => {
if (!isAdmin.value) { if (!isAdmin.value) {
message.error(t('Only system administrators can restart environment')) message.error(t('Only system administrators can restart environment'))
@ -1158,7 +1158,7 @@ const handleRestartEnvironment = () => {
}) })
} }
// // Load user account information
const loadUserAccountInfo = async () => { const loadUserAccountInfo = async () => {
userAccountInfoLoading.value = true userAccountInfoLoading.value = true
try { try {
@ -1173,22 +1173,22 @@ const loadUserAccountInfo = async () => {
profileForm.email = result.data.email || '' profileForm.email = result.data.email || ''
} }
// Team // Also get Team information
if (result.team) { if (result.team) {
teamInfo.value = result.team teamInfo.value = result.team
// // Load partner name
if (result.team.partner_email) { if (result.team.partner_email) {
await loadPartnerName() await loadPartnerName()
} }
} }
} catch (error: any) { } catch (error: any) {
console.error('加载用户账户信息失败:', error) // Failed to load user account information
} finally { } finally {
userAccountInfoLoading.value = false userAccountInfoLoading.value = false
} }
} }
// // Save profile
const saveProfile = async () => { const saveProfile = async () => {
profileSaving.value = true profileSaving.value = true
try { try {
@ -1211,7 +1211,7 @@ const saveProfile = async () => {
} }
} }
// // Become developer
const handleBecomeDeveloper = () => { const handleBecomeDeveloper = () => {
dialog.warning({ dialog.warning({
title: t('Become a Marketplace Developer?'), title: t('Become a Marketplace Developer?'),
@ -1220,25 +1220,25 @@ const handleBecomeDeveloper = () => {
negativeText: t('Cancel'), negativeText: t('Cancel'),
onPositiveClick: async () => { onPositiveClick: async () => {
if (!teamInfo.value?.name) { if (!teamInfo.value?.name) {
message.error(t('无法获取团队信息')) message.error(t('Unable to get team information'))
return return
} }
const result = await becomeDeveloper(teamInfo.value.name) const result = await becomeDeveloper(teamInfo.value.name)
if (result.success) { if (result.success) {
message.success(t('You can now publish apps to our marketplace')) 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) { if (result.data) {
// is_developer // Ensure is_developer field is set correctly
teamInfo.value = { teamInfo.value = {
...teamInfo.value, ...teamInfo.value,
...result.data, ...result.data,
is_developer: result.data.is_developer !== undefined ? result.data.is_developer : 1 is_developer: result.data.is_developer !== undefined ? result.data.is_developer : 1
} }
} else { } else {
// API is_developer // If API doesn't return data, directly set is_developer
teamInfo.value = { ...teamInfo.value, is_developer: 1 } teamInfo.value = { ...teamInfo.value, is_developer: 1 }
} }
// API 60 // Delay refresh to avoid cache issues (API has 60 second cache)
setTimeout(async () => { setTimeout(async () => {
await loadUserAccountInfo() await loadUserAccountInfo()
}, 1000) }, 1000)
@ -1249,7 +1249,7 @@ const handleBecomeDeveloper = () => {
}) })
} }
// // Remove partner
const handleRemovePartner = async () => { const handleRemovePartner = async () => {
const result = await removePartner() const result = await removePartner()
if (result.success) { if (result.success) {
@ -1261,7 +1261,7 @@ const handleRemovePartner = async () => {
} }
} }
// // Check password strength
const checkPasswordStrength = () => { const checkPasswordStrength = () => {
if (passwordStrengthTimeout.value) { if (passwordStrengthTimeout.value) {
clearTimeout(passwordStrengthTimeout.value) clearTimeout(passwordStrengthTimeout.value)
@ -1302,7 +1302,7 @@ const checkPasswordStrength = () => {
}, 200) }, 200)
} }
// // Check password mismatch
const checkPasswordMismatch = () => { const checkPasswordMismatch = () => {
if (oldPassword.value && newPassword.value && oldPassword.value === newPassword.value) { if (oldPassword.value && newPassword.value && oldPassword.value === newPassword.value) {
passwordMismatchMessage.value = t('New password cannot be the same as current password') 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 isResetPasswordFormValid = computed(() => {
const hasNewPassword = newPassword.value && newPassword.value.length > 0 const hasNewPassword = newPassword.value && newPassword.value.length > 0
const hasConfirmPassword = confirmPassword.value && confirmPassword.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 return oldPassword.value && hasNewPassword && hasConfirmPassword && passwordsMatch && passwordsDifferent
}) })
// // Handle reset password
const handleResetPassword = async () => { const handleResetPassword = async () => {
passwordError.value = '' passwordError.value = ''
@ -1397,7 +1397,7 @@ const closeResetPasswordDialog = () => {
} }
} }
// // Partner code change handler (debounce)
let partnerCodeDebounceTimer: any = null let partnerCodeDebounceTimer: any = null
const handlePartnerCodeChange = () => { const handlePartnerCodeChange = () => {
partnerExists.value = false partnerExists.value = false
@ -1427,7 +1427,7 @@ const handlePartnerCodeChange = () => {
}, 500) }, 500)
} }
// // Add partner code
const handleAddPartnerCode = async () => { const handleAddPartnerCode = async () => {
if (!partnerExists.value) { if (!partnerExists.value) {
return return
@ -1454,7 +1454,7 @@ const handleAddPartnerCode = async () => {
} }
} }
// // Close add partner code dialog
const closeAddPartnerCodeDialog = () => { const closeAddPartnerCodeDialog = () => {
showAddPartnerCodeDialog.value = false showAddPartnerCodeDialog.value = false
partnerCode.value = '' partnerCode.value = ''
@ -1467,7 +1467,7 @@ const closeAddPartnerCodeDialog = () => {
} }
} }
// // Load partner name
const loadPartnerName = async () => { const loadPartnerName = async () => {
if (teamInfo.value?.partner_email) { if (teamInfo.value?.partner_email) {
const result = await getPartnerName(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 () => { const load2FAQRCode = async () => {
if (is2FAEnabled.value) { if (is2FAEnabled.value) {
return // QR return // If already enabled, no need to load QR code
} }
loadingQRCode.value = true loadingQRCode.value = true
@ -1498,7 +1498,7 @@ const load2FAQRCode = async () => {
} }
} }
// 2FA // Enable 2FA
const handleEnable2FA = async () => { const handleEnable2FA = async () => {
if (!totpCode.value) { if (!totpCode.value) {
twoFAError.value = t('Please enter the code from the authenticator app') 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')) message.success(t('Two-factor authentication enabled successfully'))
totpCode.value = '' totpCode.value = ''
close2FADialog() close2FADialog()
// // Reload user information
setTimeout(async () => { setTimeout(async () => {
await loadUserAccountInfo() await loadUserAccountInfo()
}, 500) }, 500)
@ -1536,7 +1536,7 @@ const handleEnable2FA = async () => {
} }
} }
// 2FA // Disable 2FA
const handleDisable2FA = async () => { const handleDisable2FA = async () => {
if (!totpCode.value) { if (!totpCode.value) {
twoFAError.value = t('Please enter the code from the authenticator app') twoFAError.value = t('Please enter the code from the authenticator app')
@ -1574,7 +1574,7 @@ const handleDisable2FA = async () => {
} }
} }
// 2FA // Close 2FA dialog
const close2FADialog = () => { const close2FADialog = () => {
show2FADialog.value = false show2FADialog.value = false
totpCode.value = '' totpCode.value = ''
@ -1583,7 +1583,7 @@ const close2FADialog = () => {
showSetupKey.value = false showSetupKey.value = false
} }
// 2FA QR // Watch 2FA dialog open, load QR code
watch(show2FADialog, (newVal) => { watch(show2FADialog, (newVal) => {
if (newVal && !is2FAEnabled.value) { if (newVal && !is2FAEnabled.value) {
load2FAQRCode() load2FAQRCode()
@ -1591,7 +1591,7 @@ watch(show2FADialog, (newVal) => {
}) })
// API Secret // Create API Secret
const handleCreateSecret = async () => { const handleCreateSecret = async () => {
createSecretLoading.value = true createSecretLoading.value = true
try { try {
@ -1610,14 +1610,14 @@ const handleCreateSecret = async () => {
} }
} }
// Secret // Close create Secret dialog
const closeCreateSecretDialog = () => { const closeCreateSecretDialog = () => {
showCreateSecretDialog.value = false showCreateSecretDialog.value = false
createSecretData.value = null createSecretData.value = null
loadUserAccountInfo() loadUserAccountInfo()
} }
// SSH // Load SSH key list
const loadSSHKeys = async () => { const loadSSHKeys = async () => {
sshKeysLoading.value = true sshKeysLoading.value = true
try { try {
@ -1626,13 +1626,13 @@ const loadSSHKeys = async () => {
sshKeys.value = result.data sshKeys.value = result.data
} }
} catch (error: any) { } catch (error: any) {
console.error('加载 SSH 密钥列表失败:', error) // Failed to load SSH key list
} finally { } finally {
sshKeysLoading.value = false sshKeysLoading.value = false
} }
} }
// SSH // Add SSH key
const handleAddSSHKey = async () => { const handleAddSSHKey = async () => {
if (!sshKeyValue.value.trim()) { if (!sshKeyValue.value.trim()) {
sshKeyError.value = t('SSH key is required') sshKeyError.value = t('SSH key is required')
@ -1658,7 +1658,7 @@ const handleAddSSHKey = async () => {
} }
} }
// SSH // Set default SSH key
const handleSetDefaultSSHKey = async (keyName: string) => { const handleSetDefaultSSHKey = async (keyName: string) => {
try { try {
const result = await markKeyAsDefault(keyName) const result = await markKeyAsDefault(keyName)
@ -1673,7 +1673,7 @@ const handleSetDefaultSSHKey = async (keyName: string) => {
} }
} }
// SSH // Delete SSH key
const handleDeleteSSHKey = async (keyName: string) => { const handleDeleteSSHKey = async (keyName: string) => {
dialog.warning({ dialog.warning({
title: t('Delete SSH Key'), title: t('Delete SSH Key'),
@ -1732,25 +1732,24 @@ onMounted(async () => {
initLocale() initLocale()
systemSettings.language = getCurrentLocale() systemSettings.language = getCurrentLocale()
// 使 // Initialize timezone options (use grouped display, industry best practice)
try { try {
timezoneOptions.value = getGroupedTimezoneOptions() timezoneOptions.value = getGroupedTimezoneOptions()
} catch (error) { } catch (error) {
timezoneError.value = error instanceof Error ? error.message : String(error) timezoneError.value = error instanceof Error ? error.message : String(error)
message.error(t('Failed to load timezone options') + ': ' + timezoneError.value) 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() await loadUserAccountInfo()
// SSH // Load SSH key list
await loadSSHKeys() await loadSSHKeys()
// // Load feature flags
await loadFeatureFlags() await loadFeatureFlags()
// // If system administrator, load environment configuration (silent load, no message)
if (isAdmin.value) { if (isAdmin.value) {
await loadEnvironmentConfig(false) await loadEnvironmentConfig(false)
} }

View File

@ -218,11 +218,11 @@ const urlInputRef = ref<HTMLInputElement | null>(null)
const uploadedImage = ref<File | null>(null) const uploadedImage = ref<File | null>(null)
const uploadedImageUrl = ref<string>('') const uploadedImageUrl = ref<string>('')
const resultImage = 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 imageUrl = ref<string>('')
const resultImageUrl = computed(() => { const resultImageUrl = computed(() => {
if (!resultImage.value) return '' if (!resultImage.value) return ''
// URL // Return image URL directly
return resultImage.value return resultImage.value
}) })
@ -282,7 +282,7 @@ const handlePaste = async (event: ClipboardEvent) => {
const items = event.clipboardData?.items const items = event.clipboardData?.items
if (!items) return if (!items) return
// // Check if there is image data
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i] const item = items[i]
if (item.type.indexOf('image') !== -1) { 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') const text = event.clipboardData?.getData('text')
if (text && isValidImageUrl(text)) { if (text && isValidImageUrl(text)) {
event.preventDefault() event.preventDefault()
@ -351,7 +351,7 @@ const handleUrlSubmit = async () => {
currentHistoryIndex.value = -1 currentHistoryIndex.value = -1
try { try {
// 使 // Use proxy or load image directly
const response = await fetch(url, { mode: 'cors' }) const response = await fetch(url, { mode: 'cors' })
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load image: ${response.status}`) throw new Error(`Failed to load image: ${response.status}`)
@ -359,7 +359,7 @@ const handleUrlSubmit = async () => {
const blob = await response.blob() const blob = await response.blob()
// // Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'] const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
if (!validTypes.includes(blob.type)) { if (!validTypes.includes(blob.type)) {
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP')) message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
@ -367,7 +367,7 @@ const handleUrlSubmit = async () => {
return return
} }
// // Validate file size
const maxSize = 10 * 1024 * 1024 const maxSize = 10 * 1024 * 1024
if (blob.size > maxSize) { if (blob.size > maxSize) {
message.warning(t('Image size exceeds 10MB limit')) message.warning(t('Image size exceeds 10MB limit'))
@ -375,10 +375,10 @@ const handleUrlSubmit = async () => {
return return
} }
// File // Convert to File object
const originalFile = new File([blob], 'image-from-url', { type: blob.type }) const originalFile = new File([blob], 'image-from-url', { type: blob.type })
// 1024x1024 // Compress image to 1024x1024
try { try {
const compressedFile = await compressImageFile(originalFile, { const compressedFile = await compressImageFile(originalFile, {
maxWidth: 1024, maxWidth: 1024,
@ -390,15 +390,13 @@ const handleUrlSubmit = async () => {
uploadedImage.value = compressedFile uploadedImage.value = compressedFile
uploadedImageUrl.value = URL.createObjectURL(compressedFile) uploadedImageUrl.value = URL.createObjectURL(compressedFile)
// // Start processing
await handleRemoveBackground() await handleRemoveBackground()
} catch (error) { } catch (error) {
console.error('图片压缩失败:', error) message.error(t('Image processing failed, please try again'))
message.error('图片处理失败,请重试')
processing.value = false processing.value = false
} }
} catch (error: any) { } catch (error: any) {
console.error('加载图片URL失败:', error)
let errorMessage = t('Failed to load image from URL') let errorMessage = t('Failed to load image from URL')
if (error.message?.includes('CORS')) { if (error.message?.includes('CORS')) {
@ -420,11 +418,11 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
window.removeEventListener('paste', handlePaste) window.removeEventListener('paste', handlePaste)
// URL // Clean up object URL
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) { if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(uploadedImageUrl.value) URL.revokeObjectURL(uploadedImageUrl.value)
} }
// blob URL // Clean up result image blob URL cache
if (resultImageBlobUrl.value) { if (resultImageBlobUrl.value) {
URL.revokeObjectURL(resultImageBlobUrl.value) URL.revokeObjectURL(resultImageBlobUrl.value)
} }
@ -497,7 +495,7 @@ const processFile = async (file: File) => {
return return
} }
// 1024x1024 // Compress image to 1024x1024
try { try {
const compressedFile = await compressImageFile(file, { const compressedFile = await compressImageFile(file, {
maxWidth: 1024, maxWidth: 1024,
@ -518,17 +516,16 @@ const processFile = async (file: File) => {
} }
reader.readAsDataURL(compressedFile) reader.readAsDataURL(compressedFile)
} catch (error) { } catch (error) {
console.error('图片压缩失败:', error) message.error(t('Image processing failed, please try again'))
message.error('图片处理失败,请重试')
} }
} }
const resetUpload = () => { const resetUpload = () => {
// URL // Clean up object URL
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) { if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(uploadedImageUrl.value) URL.revokeObjectURL(uploadedImageUrl.value)
} }
// blob URL // Clean up result image blob URL cache
if (resultImageBlobUrl.value) { if (resultImageBlobUrl.value) {
URL.revokeObjectURL(resultImageBlobUrl.value) URL.revokeObjectURL(resultImageBlobUrl.value)
resultImageBlobUrl.value = '' resultImageBlobUrl.value = ''
@ -558,7 +555,7 @@ const selectHistoryItem = async (index: number) => {
splitPosition.value = 0 splitPosition.value = 0
uploadedImage.value = item.originalImageFile uploadedImage.value = item.originalImageFile
// // Cache result image from history
if (item.resultImage) { if (item.resultImage) {
await cacheResultImage(item.resultImage) await cacheResultImage(item.resultImage)
} }
@ -608,7 +605,7 @@ const handleRemoveBackground = async () => {
processing.value = true processing.value = true
resultImage.value = '' resultImage.value = ''
// 使 // Helper function to handle successful result (internal use)
const handleSuccess = async (imageUrl: string): Promise<void> => { const handleSuccess = async (imageUrl: string): Promise<void> => {
resultImage.value = imageUrl resultImage.value = imageUrl
await cacheResultImage(imageUrl) await cacheResultImage(imageUrl)
@ -674,13 +671,13 @@ const handleRemoveBackground = async () => {
message.error(result.error) message.error(result.error)
} }
} catch (parseError) { } catch (parseError) {
console.error('Failed to parse JSON:', parseError, 'Line:', line) // Failed to parse JSON, continue processing
} }
} }
} }
} }
// // Process last line
if (buffer.trim()) { if (buffer.trim()) {
try { try {
const result = JSON.parse(buffer.trim()) const result = JSON.parse(buffer.trim())
@ -691,7 +688,6 @@ const handleRemoveBackground = async () => {
message.error(result.error || t('Failed to remove background')) message.error(result.error || t('Failed to remove background'))
} }
} catch (parseError) { } catch (parseError) {
console.error('Failed to parse final JSON:', parseError)
message.error(t('Failed to parse response')) message.error(t('Failed to parse response'))
} }
} else { } else {
@ -713,17 +709,17 @@ const handleRemoveBackground = async () => {
} }
/** /**
* 缓存结果图片为 blob URL用于下载 * Cache result image as blob URL for download
*/ */
const cacheResultImage = async (imageUrl: string) => { const cacheResultImage = async (imageUrl: string) => {
try { try {
// blob URL // Clean up old blob URL
if (resultImageBlobUrl.value) { if (resultImageBlobUrl.value) {
URL.revokeObjectURL(resultImageBlobUrl.value) URL.revokeObjectURL(resultImageBlobUrl.value)
resultImageBlobUrl.value = '' resultImageBlobUrl.value = ''
} }
// blob URL // Fetch image and convert to blob URL
const response = await fetch(imageUrl) const response = await fetch(imageUrl)
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status}`) throw new Error(`Failed to fetch image: ${response.status}`)
@ -731,8 +727,7 @@ const cacheResultImage = async (imageUrl: string) => {
const blob = await response.blob() const blob = await response.blob()
resultImageBlobUrl.value = URL.createObjectURL(blob) resultImageBlobUrl.value = URL.createObjectURL(blob)
} catch (error) { } 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 if (!resultImage.value) return
try { try {
// 使 blob URL // Prefer cached blob URL
let blobUrl = resultImageBlobUrl.value let blobUrl = resultImageBlobUrl.value
// // If no cache, fetch and cache
if (!blobUrl) { if (!blobUrl) {
const response = await fetch(resultImage.value) const response = await fetch(resultImage.value)
if (!response.ok) { if (!response.ok) {
@ -754,7 +749,7 @@ const handleDownload = async () => {
resultImageBlobUrl.value = blobUrl resultImageBlobUrl.value = blobUrl
} }
// // Create download link
const link = document.createElement('a') const link = document.createElement('a')
link.href = blobUrl link.href = blobUrl
link.download = `removed-background-${Date.now()}.png` link.download = `removed-background-${Date.now()}.png`
@ -763,12 +758,11 @@ const handleDownload = async () => {
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
// DOM blob URL使 // Clean up: remove DOM element but don't release blob URL (keep cache for subsequent downloads)
requestAnimationFrame(() => { requestAnimationFrame(() => {
document.body.removeChild(link) document.body.removeChild(link)
}) })
} catch (error: any) { } catch (error: any) {
console.error('下载图片失败:', error)
message.error(t('Failed to download image')) message.error(t('Failed to download image'))
} }
} }