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 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;
}

View File

@ -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)
}

View File

@ -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'))
}
}