refactor: 优化前端代码,准备生产环境部署
- 删除所有调试日志(console.error/console.log) - 将所有中文注释改为英文注释 - 将硬编码的中文文本改为使用 t() 函数的英文,支持国际化 - 移除可能暴露项目信息的内容 - 添加新增翻译键的中文翻译(Portrait Sample, Product Sample, Animal Sample, Object Sample, Unable to get team information) 修改文件: - src/views/HomePage.vue - src/views/tools/remove_background/remove_background.vue - src/views/settings/Settings.vue - src/locales/zh-CN.json
This commit is contained in:
parent
36af166879
commit
ee2f59df81
@ -1386,5 +1386,10 @@
|
|||||||
"请选择一个原因": "请选择一个原因",
|
"请选择一个原因": "请选择一个原因",
|
||||||
"请评价您的体验": "请评价您的体验",
|
"请评价您的体验": "请评价您的体验",
|
||||||
"请简要说明原因": "请简要说明原因",
|
"请简要说明原因": "请简要说明原因",
|
||||||
"Your feedback has been submitted successfully": "您的反馈已成功提交"
|
"Your feedback has been submitted successfully": "您的反馈已成功提交",
|
||||||
|
"Portrait Sample": "人物示例",
|
||||||
|
"Product Sample": "产品示例",
|
||||||
|
"Animal Sample": "动物示例",
|
||||||
|
"Object Sample": "物品示例",
|
||||||
|
"Unable to get team information": "无法获取团队信息"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
|||||||
const currentYear = computed(() => new Date().getFullYear())
|
const 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user