Compare commits

...

14 Commits
dev ... main

Author SHA1 Message Date
f092728a04 change FRONTEND_PORT to 3001 2026-01-11 00:20:28 +08:00
8820fc8e06 添加站点统计代码 2026-01-09 18:13:34 +08:00
de92a1e63d 删除英文版首页的jingrow链接 2026-01-04 23:24:52 +08:00
c489d41941 fix: 修复手机端图片等比例缩小和拖拽按钮触摸支持
- 移除 .comparison-image img 的 min-width 和 min-height,修复手机端图片无法等比例缩小的问题
- 为拖拽分割线添加触摸事件支持(touchstart/touchmove/touchend),使手机端可以正常拖拽对比视图
2026-01-04 22:41:47 +08:00
ee2f59df81 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
2026-01-04 22:19:19 +08:00
36af166879 删除未使用的通知铃铛图标
- 移除右上角未使用的通知按钮(铃铛图标)
- 简化头部右侧布局,仅保留搜索框和用户菜单
2026-01-04 21:45:17 +08:00
6e6d822b06 统一卡片风格并补充缺失的中文翻译
- 统一推荐有礼和合作伙伴卡片的风格,使用与功能设置卡片一致的 n-list 和 n-list-item 结构
- 添加 API key created successfully 的中文翻译
- 添加 Failed to create API key 的中文翻译
- 添加 Partner removed successfully 的中文翻译
- 添加 Failed to remove partner 的中文翻译
- 添加 Delete SSH Key 的中文翻译
- 添加 Only system administrators can edit environment configuration 的中文翻译
2026-01-04 21:42:22 +08:00
dfad1a1d80 删除禁用账户功能及相关代码
- 删除个人资料标签页中的禁用/启用账户列表项
- 删除禁用账户对话框和启用账户对话框
- 删除反馈对话框(禁用账户后显示)
- 删除账户状态相关的状态变量和计算属性
- 删除 handleAccountStatus、handleDisableAccountConfirm、handleEnableAccountConfirm 等处理函数
- 删除反馈相关的状态变量和处理函数(feedbackRating、feedbackReason、feedbackNote 等)
- 从 API 模块中删除 disableAccount、enableAccount、submitFeedback 函数
- 删除账户状态对话框和星级评分的 CSS 样式
2026-01-04 21:34:33 +08:00
423e28c09c 删除 Dashboard 页面,将首页改为根目录
- 删除 Dashboard.vue 页面文件
- 修改路由配置:删除 Dashboard 路由,根路径重定向到 /tools
- 删除菜单配置中的 dashboard 菜单项
- 删除 AppHeader 中的 Dashboard 标题映射
- 修复禁用账户后跳转问题:反馈提交后跳转到根目录而非 /dashboard
2026-01-04 21:29:51 +08:00
f30502bf7d fix(settings): fix disable account API and add feedback dialog
- Fix disable account API parameter format (pass null instead of empty object)
- Add feedback dialog after account disable (aligned with jcloud dashboard)
- Implement star rating, reason selection, and feedback submission
- Add submitFeedback API function
- Add missing Chinese translations for account status messages
- Add feedback dialog translations and validation logic
2026-01-04 21:23:29 +08:00
79b92e7aae fix(settings): improve profile settings UI and i18n support
- Remove First Name and Last Name fields from Update Profile Information dialog
- Add i18n support for copy button messages (Copied to clipboard, Copy failed)
- Add Chinese translation for 'Update Profile Information'
- Fix QR code centering in 2FA dialog
- Fix disable account dialog formatting with proper line breaks
- Change disable account dialog icon type to warning for semantic correctness
2026-01-04 20:50:22 +08:00
911ae5e53b feat: 系统设置页面增加个人资料和开发者标签页
- 使用 naive-ui Tabs 组件实现标签页布局,对齐 jcloud dashboard 前端
- 个人资料标签页:显示和编辑用户信息(用户名、手机、邮箱)、邮箱通知设置
- 开发者标签页:API 访问(创建/重新生成 API Key)、SSH 密钥管理、功能标志配置
- 创建账户相关 API 接口文件(account.ts),使用 jcloud.api.account.get 获取用户信息
- 创建 ClickToCopyField 组件用于一键复制文本内容
- API 接口与 jcloud dashboard 保持一致,实现相同的功能
2026-01-04 19:54:02 +08:00
4b3ebaa7ed refactor: 移除登录信息在localStorage中的保存,改为仅使用cookies验证
- 移除 auth store 中所有 localStorage 相关代码(loadUserFromStorage、saveUserToStorage、clearUserFromStorage)
- 简化 setUserState 和 clearUserState,不再操作 localStorage
- 修改 initAuth 逻辑,只依赖 cookies 验证登录状态,不再从 localStorage 恢复
- 移除 HomePage.vue 和 Signup.vue 中注册成功后的 localStorage 保存
- 登录状态完全由后端 cookies 控制,提高安全性
2026-01-04 19:16:37 +08:00
eb70a0c6f6 fix: 修复开发环境登录后无法获取cookie的问题
- 为 /api/action 和 /api/data 代理添加 cookieDomainRewrite 和 cookiePathRewrite 配置
- 将 secure 设置为 false 以支持开发环境的证书验证
- 确保后端返回的 cookie 能在开发服务器正确传递和保存
2026-01-04 19:11:35 +08:00
14 changed files with 2633 additions and 625 deletions

View File

@ -9,5 +9,14 @@
<body>
<div id="app"></div>
<script type="module" src="/src/app/main.ts"></script>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?37b6ba3388e71f7dfc30535b280eb067";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>

View File

@ -44,13 +44,6 @@
</template>
</n-input>
<!-- 通知 -->
<n-button quaternary circle>
<template #icon>
<Icon icon="tabler:bell" />
</template>
</n-button>
<!-- 用户菜单 -->
<UserMenu />
</n-space>
@ -181,7 +174,6 @@ const breadcrumbItems = computed(() => {
} else {
//
const map: Record<string, string> = {
Dashboard: t('Dashboard'),
AgentList: t('Agents'),
AgentDetail: t('Agent Detail'),
NodeList: t('Node Management'),

View File

@ -29,9 +29,9 @@ const router = createRouter({
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('../../views/Dashboard.vue')
path: '',
name: 'Home',
redirect: '/tools'
},
{
path: 'local-jobs',

View File

@ -21,6 +21,7 @@
"Agent Detail": "智能体详情",
"Flow Builder": "流程编排",
"Profile": "个人资料",
"Update Profile Information": "更新个人资料信息",
"Logout": "退出登录",
"Logged out": "已退出登录",
"AI Agent Workflow Platform": "AI Agent 工作流平台",
@ -355,6 +356,8 @@
"Unsaved": "未保存",
"Saved": "已保存",
"Copy failed, please copy manually": "复制失败,请手动复制",
"Copied to clipboard": "已复制到剪贴板",
"Copy failed": "复制失败",
"All": "全部",
"Recent": "最近使用",
"Favorites": "收藏",
@ -966,6 +969,7 @@
"Restart Environment": "重启环境",
"Restart": "重启",
"Only system administrators can restart environment": "仅系统管理员可以重启环境",
"Only system administrators can edit environment configuration": "仅系统管理员可以查看和编辑环境配置",
"Are you sure you want to restart the environment? This operation may cause service interruption.": "您确定要重启环境吗?此操作可能导致服务中断。",
"Environment restart request submitted. The system will restart shortly.": "环境重启请求已提交,系统将在稍后重启。",
"Failed to restart environment": "重启环境失败",
@ -1237,5 +1241,155 @@
"Online Support": "在线客服",
"Feedback & Suggestions": "反馈建议",
"Guangzhou Sunflower Network Information Technology Co., Ltd.": "广州向日葵网络信息技术有限公司",
"All Rights Reserved": "版权所有"
"All Rights Reserved": "版权所有",
"API Access": "API 访问",
"API key and API secret can be used to access": "API 密钥和 API 密钥可用于访问",
"API key and API secret pairs can be used to access the Jingrow API.": "API 密钥和 API 密钥对可用于访问 Jingrow API。",
"Jingrow API": "Jingrow API",
"Create New API Key": "创建新的 API 密钥",
"Regenerate API Key": "重新生成 API 密钥",
"API key created successfully": "API 密钥创建成功",
"Failed to create API key": "创建 API 密钥失败",
"You don't have an API key yet. Click the button above to create one.": "您还没有 API 密钥。请点击上方按钮创建一个。",
"Please copy the API secret now. You won't be able to see it again!": "请立即复制 API 密钥。您将无法再次查看它!",
"API Secret": "API 密钥",
"SSH Keys": "SSH 密钥",
"Add SSH Key": "添加 SSH 密钥",
"Add New SSH Key": "添加新的 SSH 密钥",
"SSH Key": "SSH 密钥",
"SSH Fingerprint": "SSH 指纹",
"Added Time": "添加时间",
"Set as Default": "设为默认",
"No SSH keys configured": "未配置 SSH 密钥",
"Add a new SSH key to your account": "为您的账户添加新的 SSH 密钥",
"SSH key is required": "SSH 密钥是必填项",
"Link Partner Account": "关联合作伙伴账户",
"Enter the partner code provided by your partner": "输入您的合作伙伴提供的合作伙伴代码",
"Partner Code": "合作伙伴代码",
"For example: rGjw3hJ81b": "例如rGjw3hJ81b",
"Submit": "提交",
"Remove Partner": "移除合作伙伴",
"This will remove the partner associated with your account. Are you sure you want to remove this partner?": "这将移除与您的账户关联的合作伙伴。确定要移除此合作伙伴吗?",
"Partner removed successfully": "合作伙伴移除成功",
"Failed to remove partner": "移除合作伙伴失败",
"SSH key added successfully": "SSH 密钥添加成功",
"Failed to add SSH key": "添加 SSH 密钥失败",
"SSH key updated successfully": "SSH 密钥更新成功",
"Failed to set SSH key as default": "设置默认 SSH 密钥失败",
"SSH key deleted successfully": "SSH 密钥删除成功",
"Failed to delete SSH key": "删除 SSH 密钥失败",
"Are you sure you want to delete this SSH key?": "确定要删除此 SSH 密钥吗?",
"Delete SSH Key": "删除 SSH 密钥",
"Advanced Features": "高级功能",
"Enable private benches": "启用私有工作台",
"Enable security portal": "启用安全门户",
"Save Changes": "保存更改",
"Feature flags updated successfully": "功能标志更新成功",
"Failed to update feature flags": "更新功能标志失败",
"Marketplace Developer": "应用市场开发者",
"Developers can publish their apps on the marketplace for users to subscribe to, either paid or free.": "开发者可以在应用市场发布自己的应用,供用户付费或免费订阅。",
"Become a Developer": "成为开发者",
"Become a Marketplace Developer?": "成为应用市场开发者?",
"After confirmation, you will be able to publish apps to our marketplace.": "确认后,您将能够在我们的应用市场发布应用。",
"Enable Two-Factor Authentication": "启用双因素认证",
"Disable Two-Factor Authentication": "禁用双因素认证",
"Enable two-factor authentication for your account to add an extra layer of security": "为您的账户启用双因素认证以增加额外的安全层",
"Disable two-factor authentication for your account": "为您的账户禁用双因素认证",
"Enable": "启用",
"Disable": "禁用",
"Reset Password": "重置密码",
"Change your account login password": "更改您的账户登录密码",
"Disable Account": "禁用账户",
"Enable Account": "启用账户",
"Your account has been disabled successfully": "您的账户已成功禁用",
"Failed to disable account": "禁用账户失败",
"Your account has been enabled successfully": "您的账户已成功启用",
"Failed to enable account": "启用账户失败",
"Disable your account and stop billing": "禁用您的账户并停止计费",
"Enable your account and resume billing": "启用您的账户并恢复计费",
"After confirming this action:": "确认此操作后:",
"Your account will be disabled": "您的账户将被禁用",
"Your activated sites will be suspended immediately and deleted after one week.": "您已激活的站点将立即暂停,并在一周后删除。",
"Your account billing will stop": "您的账户计费将停止",
"You can log in later to re-enable your account. Do you want to continue?": "您可以稍后登录以重新启用您的账户。是否继续?",
"Your account will be enabled": "您的账户将被启用",
"Your suspended sites will be reactivated": "您已暂停的站点将被重新激活",
"Your account billing will resume": "您的账户计费将恢复",
"Do you want to continue?": "是否继续?",
"Referral Program": "推荐有礼",
"Your exclusive referral link": "您的专属推荐链接",
"Invite others to join Jingrow,": "邀请他人加入 Jingrow",
"when they register and spend at least ¥100, you will get ¥20": "当他们注册并消费至少¥100时您将获得¥20",
"Jingrow Partner": "Jingrow 合作伙伴",
"Jingrow partner associated with your account": "与您的账户关联的 Jingrow 合作伙伴",
"Add Partner Code": "添加合作伙伴代码",
"Unlink Partner": "取消关联合作伙伴",
"Have a Jingrow partner referral code? Click": "有 Jingrow 合作伙伴推荐代码吗?点击",
"to associate with your partner team.": "以与您的合作伙伴团队关联。",
"Feature coming soon": "功能即将推出",
"is an invalid referral code": "是无效的推荐代码",
"Current Password": "当前密码",
"New Password": "新密码",
"Please enter current password": "请输入当前密码",
"Please enter new password": "请输入新密码",
"Please re-enter new password": "请重新输入新密码",
"New password cannot be the same as current password": "新密码不能与当前密码相同",
"Password strength is good 👍": "密码强度良好 👍",
"Tip: Password should contain symbols, numbers and uppercase letters": "提示:密码应包含符号、数字和大写字母",
"Password updated successfully": "密码更新成功",
"Your password has been updated": "您的密码已更新",
"Profile updated successfully": "个人资料更新成功",
"Current password is incorrect": "当前密码不正确",
"Setting you as a developer...": "正在将您设置为开发者...",
"You can now publish apps to our marketplace": "您现在可以在我们的应用市场发布应用了",
"Failed to mark you as a developer": "将您设置为开发者失败",
"Approval Request has already been sent to Partner": "批准请求已发送给合作伙伴",
"Approval Request has been sent to Partner": "批准请求已发送给合作伙伴",
"If you disable two-factor authentication, your account will become insecure": "如果您禁用双因素认证,您的账户将变得不安全",
"Steps to Disable Two-Factor Authentication": "禁用双因素认证的步骤",
"Open the authenticator app": "打开身份验证器应用",
"Enter the code from the app below": "在下方输入应用中的代码",
"Verify the code in the app to disable two-factor authentication": "输入应用中的验证码以禁用双因素认证",
"Steps to Enable Two-Factor Authentication": "启用双因素认证的步骤",
"Download an authenticator app on your phone, such as Alibaba Cloud APP, etc.": "在手机上下载身份验证器应用,例如阿里云 APP 等",
"Scan the QR code": "扫描二维码",
"Enter the code from the authenticator app": "输入身份验证器应用中的代码",
"Enter the code from the authenticator app below": "在下方输入身份验证器应用中的代码",
"Note": "注意",
"If you cannot access the authenticator app, your account will be locked. Please ensure you back up your vault/key.": "如果您无法访问身份验证器应用,您的账户将被锁定。请确保您已备份您的密钥。",
"Setup Key": "设置密钥",
"Verify the code in the app to enable two-factor authentication": "输入应用中的验证码以启用双因素认证",
"Please enter the code from the authenticator app": "请输入身份验证器应用中的代码",
"Two-factor authentication enabled successfully": "双因素认证已成功启用",
"Two-factor authentication disabled successfully": "双因素认证已成功禁用",
"Invalid TOTP code, please try again": "无效的 TOTP 代码,请重试",
"Failed to enable two-factor authentication": "启用双因素认证失败",
"Failed to disable two-factor authentication": "禁用双因素认证失败",
"Failed to load QR code": "加载二维码失败",
"Enabling two-factor authentication...": "正在启用双因素认证...",
"Disabling two-factor authentication...": "正在禁用双因素认证...",
"Tell us why you are leaving": "告诉我们您离开的原因",
"By sharing your thoughts, help us improve your experience.": "通过分享您的想法,帮助我们改善您的体验。",
"Please rate your experience": "请评价您的体验",
"Select a reason": "选择一个原因",
"The reason I am leaving Jingrow is...": "我离开 Jingrow 的原因是...",
"我要迁移到其他产品": "我要迁移到其他产品",
"我只是在探索这个产品": "我只是在探索这个产品",
"我更喜欢自己托管实例": "我更喜欢自己托管实例",
"已将站点迁移到另一个Jingrow账户": "已将站点迁移到另一个Jingrow账户",
"我不喜欢Jingrow的体验": "我不喜欢Jingrow的体验",
"Jingrow对我来说太贵了": "Jingrow对我来说太贵了",
"支付问题": "支付问题",
"缺少功能": "缺少功能",
"我的原因不在此列表中": "我的原因不在此列表中",
"请选择一个原因": "请选择一个原因",
"请评价您的体验": "请评价您的体验",
"请简要说明原因": "请简要说明原因",
"Your feedback has been submitted successfully": "您的反馈已成功提交",
"Portrait Sample": "人物示例",
"Product Sample": "产品示例",
"Animal Sample": "动物示例",
"Object Sample": "物品示例",
"Unable to get team information": "无法获取团队信息"
}

514
src/shared/api/account.ts Normal file
View File

@ -0,0 +1,514 @@
import axios from 'axios'
import { get_session_api_headers } from './auth'
// 创建或重新生成 API Secret
export const createApiSecret = async (): Promise<{ success: boolean; data?: { api_key: string; api_secret: string }; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.create_api_secret`,
{},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data
if (result?.message) {
return { success: true, data: result.message }
}
return { success: true, data: result }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '创建 API Secret 失败'
}
}
}
// 更新个人资料
export interface UpdateProfileParams {
first_name?: string
last_name?: string
username?: string
mobile_no?: string
email?: string
}
export const updateProfile = async (params: UpdateProfileParams): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.update_profile`,
params,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '更新成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '更新个人资料失败'
}
}
}
// 获取邮箱列表
export const getEmails = async (): Promise<{ success: boolean; data?: Array<{ type: string; value: string }>; message?: string }> => {
try {
const response = await axios.get(
`/api/action/jcloud.api.account.get_emails`,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data
return { success: true, data: result?.message || result }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '获取邮箱列表失败'
}
}
}
// 更新邮箱
export const updateEmails = async (data: Array<{ type: string; value: string }>): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.update_emails`,
{ data: JSON.stringify(data) },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '更新成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '更新邮箱失败'
}
}
}
// 获取用户 SSH 密钥列表
export const getUserSSHKeys = async (): Promise<{ success: boolean; data?: Array<any>; message?: string }> => {
try {
const response = await axios.get(
`/api/action/jcloud.api.account.get_user_ssh_keys`,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data
return { success: true, data: result?.message || result || [] }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '获取 SSH 密钥列表失败'
}
}
}
// 添加 SSH 密钥
export const addSSHKey = async (sshPublicKey: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.client.insert`,
{
pg: {
pagetype: 'User SSH Key',
ssh_public_key: sshPublicKey,
user: '' // 后端会自动获取当前用户
}
},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '添加成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '添加 SSH 密钥失败'
}
}
}
// 设置 SSH 密钥为默认
export const markKeyAsDefault = async (keyName: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.mark_key_as_default`,
{ key_name: keyName },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '设置成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '设置默认密钥失败'
}
}
}
// 删除 SSH 密钥
export const deleteSSHKey = async (keyName: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.client.delete`,
{
pagetype: 'User SSH Key',
name: keyName
},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '删除成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '删除 SSH 密钥失败'
}
}
}
// 获取功能标志
export const getFeatureFlags = async (): Promise<{ success: boolean; data?: Record<string, boolean>; message?: string }> => {
try {
// 这里需要根据实际 API 调整
const response = await axios.get(
`/api/action/jcloud.api.account.get_feature_flags`,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data
return { success: true, data: result?.message || result }
} catch (error: any) {
// 如果 API 不存在,返回空对象
return { success: false, data: {}, message: '功能标志 API 不可用' }
}
}
// 更新功能标志
export const updateFeatureFlags = async (values: Record<string, boolean>): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.update_feature_flags`,
{ values },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '更新成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '更新功能标志失败'
}
}
}
// 获取用户信息(包含 API Key
// 使用 jcloud.api.account.get API 获取账户信息
export const getUserAccountInfo = async (): Promise<{ success: boolean; data?: any; team?: any; message?: string }> => {
try {
const response = await axios.get(
`/api/action/jcloud.api.account.get`,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
if (result?.user) {
return { success: true, data: result.user, team: result.team }
}
return { success: false, message: 'API 返回的数据中未找到用户信息' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '获取用户信息失败'
}
}
}
// 成为开发者(更新 Team 的 is_developer 字段)
// 使用 jcloud.api.client.set_value API与 jcloud dashboard 保持一致
export const becomeDeveloper = async (teamName: string): Promise<{ success: boolean; data?: any; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.client.set_value`,
{
pagetype: 'Team',
name: teamName,
fieldname: { is_developer: 1 }
},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
// set_value API 返回更新后的 Team 对象
const result = response.data?.message || response.data
return { success: true, data: result, message: '成为开发者成功' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '成为开发者失败'
}
}
}
// 更新密码
export const updatePassword = async (params: {
old_password: string
new_password: string
confirm_password: string
logout_all_sessions?: number
}): Promise<{ success: boolean; message?: string; redirectUrl?: string }> => {
try {
const response = await axios.post(
`/api/action/jingrow.core.pagetype.user.user.update_password`,
{
old_password: params.old_password,
new_password: params.new_password,
confirm_password: params.confirm_password,
logout_all_sessions: params.logout_all_sessions || 1
},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
return {
success: true,
message: '密码更新成功',
redirectUrl: typeof result === 'string' ? result : undefined
}
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '更新密码失败'
}
}
}
// 测试密码强度
export const testPasswordStrength = async (params: {
old_password: string
new_password: string
}): Promise<{ success: boolean; data?: { score: number; feedback: any }; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jingrow.core.pagetype.user.user.test_password_strength`,
{
old_password: params.old_password,
new_password: params.new_password
},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
return { success: true, data: result }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '测试密码强度失败'
}
}
}
// 验证合作伙伴代码
export const validatePartnerCode = async (code: string): Promise<{ success: boolean; isValid: boolean; partnerName?: string; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.partner.validate_partner_code`,
{ code },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
if (Array.isArray(result) && result.length >= 2) {
return {
success: true,
isValid: result[0] === true,
partnerName: result[1] || undefined
}
}
return { success: true, isValid: false }
} catch (error: any) {
return {
success: false,
isValid: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '验证合作伙伴代码失败'
}
}
}
// 添加合作伙伴代码
export const addPartnerCode = async (referralCode: string): Promise<{ success: boolean; message?: string; isAlreadySent?: boolean }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.partner.add_partner`,
{ referral_code: referralCode },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
if (result === 'Request already sent') {
return { success: true, message: '请求已发送', isAlreadySent: true }
}
return { success: true, message: '请求已发送' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '添加合作伙伴代码失败'
}
}
}
// 移除合作伙伴
export const removePartner = async (): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.partner.remove_partner`,
{},
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '合作伙伴已移除' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '移除合作伙伴失败'
}
}
}
// 获取合作伙伴名称
export const getPartnerName = async (partnerEmail: string): Promise<{ success: boolean; data?: string; message?: string }> => {
try {
const response = await axios.get(
`/api/action/jcloud.api.partner.get_partner_name`,
{
params: { partner_email: partnerEmail },
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
return { success: true, data: result }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '获取合作伙伴名称失败'
}
}
}
// 获取 2FA QR 码 URL
export const get2FAQRCodeUrl = async (): Promise<{ success: boolean; data?: string; message?: string }> => {
try {
const response = await axios.get(
`/api/action/jcloud.api.account.get_2fa_qr_code_url`,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
const result = response.data?.message || response.data
return { success: true, data: result }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '获取 2FA QR 码失败'
}
}
}
// 启用 2FA
export const enable2FA = async (totpCode: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.enable_2fa`,
{ totp_code: totpCode },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '双因素认证已启用' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '启用双因素认证失败'
}
}
}
// 禁用 2FA
export const disable2FA = async (totpCode: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await axios.post(
`/api/action/jcloud.api.account.disable_2fa`,
{ totp_code: totpCode },
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: response.data?.message || '双因素认证已禁用' }
} catch (error: any) {
return {
success: false,
message: error.response?.data?.detail || error.response?.data?.message || error.message || '禁用双因素认证失败'
}
}
}

View File

@ -0,0 +1,79 @@
<template>
<div class="click-to-copy-field">
<n-input
:value="textContent"
readonly
:placeholder="placeholder"
class="copy-input"
>
<template #suffix>
<n-button
quaternary
size="small"
@click="handleCopy"
:loading="copying"
class="copy-button"
>
<template #icon>
<n-icon>
<Icon :icon="copied ? 'tabler:check' : 'tabler:copy'" />
</n-icon>
</template>
</n-button>
</template>
</n-input>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { NInput, NButton, NIcon, useMessage } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { t } from '../i18n'
interface Props {
textContent: string
placeholder?: string
}
const props = withDefaults(defineProps<Props>(), {
placeholder: ''
})
const message = useMessage()
const copying = ref(false)
const copied = ref(false)
const handleCopy = async () => {
if (!props.textContent) return
copying.value = true
try {
await navigator.clipboard.writeText(props.textContent)
copied.value = true
message.success(t('Copied to clipboard'))
setTimeout(() => {
copied.value = false
}, 2000)
} catch (error) {
message.error(t('Copy failed'))
} finally {
copying.value = false
}
}
</script>
<style scoped>
.click-to-copy-field {
width: 100%;
}
.copy-input {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
}
.copy-button {
margin-right: 4px;
}
</style>

View File

@ -8,8 +8,6 @@ export interface User {
user_type: string
}
const STORAGE_KEY = 'auth_user'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const loading = ref(false)
@ -25,49 +23,16 @@ export const useAuthStore = defineStore('auth', () => {
error?.message?.includes('Cookie已过期')
}
// 从 localStorage 加载用户信息
const loadUserFromStorage = (): User | null => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
return JSON.parse(stored)
}
} catch (error) {
console.error('从 localStorage 加载用户信息失败:', error)
}
return null
}
// 保存用户信息到 localStorage
const saveUserToStorage = (userInfo: User) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(userInfo))
} catch (error) {
console.error('保存用户信息到 localStorage 失败:', error)
}
}
// 清除 localStorage 中的用户信息
const clearUserFromStorage = () => {
try {
localStorage.removeItem(STORAGE_KEY)
} catch (error) {
console.error('清除 localStorage 用户信息失败:', error)
}
}
// 设置用户状态统一的状态更新方法保存到localStorage
// 设置用户状态(只更新内存状态,不保存到 localStorage
const setUserState = (userInfo: User) => {
user.value = userInfo
isAuthenticated.value = true
saveUserToStorage(userInfo)
}
// 清除用户状态
const clearUserState = () => {
user.value = null
isAuthenticated.value = false
clearUserFromStorage()
}
// 验证并更新用户信息
@ -131,21 +96,8 @@ export const useAuthStore = defineStore('auth', () => {
}
}
// 如果cookie验证失败或没有cookie尝试从localStorage恢复
const storedUser = loadUserFromStorage()
if (storedUser) {
user.value = storedUser
isAuthenticated.value = true
// 尝试在后台验证用户信息,失败也不影响当前状态
validateAndUpdateUser().catch(() => {
// 静默失败保持localStorage中的状态
})
} else {
// 既没有cookie也没有localStorage清除认证状态
if (isAuthenticated.value) {
clearUserState()
}
}
// 如果没有cookie或cookie验证失败清除认证状态
clearUserState()
} finally {
setInitializingAuth(false)
}

View File

@ -50,7 +50,6 @@ function saveToStorage(items: AppMenuItem[]) {
// - 非 System User 只能看到工具市场
function getDefaultMenus(): AppMenuItem[] {
return [
{ id: 'dashboard', key: 'Dashboard', label: 'Dashboard', icon: 'tabler:dashboard', routeName: 'Dashboard', order: 1, type: 'route' },
{ id: 'work', key: 'work', label: 'Work', icon: 'tabler:device-desktop', type: 'workspace', workspaceName: 'work', url: '/workspace/work', order: 2 },
{ id: 'design', key: 'design', label: 'Design', icon: 'tabler:pencil', type: 'workspace', workspaceName: 'design', url: '/workspace/design', order: 3 },
{ id: 'website', key: 'website', label: 'Website', icon: 'tabler:world', type: 'workspace', workspaceName: 'jsite', url: '/workspace/jsite', order: 4 },

View File

@ -1,213 +0,0 @@
<template>
<div class="dashboard-page">
<div class="page-header">
<h1 class="page-title">{{ t('Dashboard') }}</h1>
</div>
<!-- 统计卡片 -->
<n-grid :cols="4" :x-gap="16" :y-gap="16" :responsive="'screen'" :item-responsive="true" class="stats-grid">
<!-- 原来的4个统计 -->
<n-grid-item>
<n-card>
<n-statistic :label="t('Total Agents')" :value="stats.agents" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('Total Nodes')" :value="stats.nodes" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('Task Queue')" :value="stats.taskQueue" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('Scheduled Tasks')" :value="stats.scheduledTasks" />
</n-card>
</n-grid-item>
<!-- 新增的5个统计 -->
<n-grid-item>
<n-card>
<n-statistic :label="t('Knowledge Base')" :value="stats.knowledgeBase" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('Note')" :value="stats.note" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('Event')" :value="stats.event" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('ToDo')" :value="stats.todo" />
</n-card>
</n-grid-item>
<n-grid-item>
<n-card>
<n-statistic :label="t('File')" :value="stats.file" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import {
NGrid,
NGridItem,
NCard,
NStatistic
} from 'naive-ui'
import { t } from '../shared/i18n'
import { getCount, getLocalJobCount } from '../shared/api/common'
const stats = reactive({
agents: 0,
nodes: 0,
taskQueue: 0,
scheduledTasks: 0,
knowledgeBase: 0,
note: 0,
event: 0,
todo: 0,
file: 0
})
//
const loadStats = async () => {
try {
// 4
//
const agentsResult = await getCount('Local Ai Agent')
if (agentsResult.success) {
stats.agents = agentsResult.count || 0
}
//
const nodesResult = await getCount('Local Ai Node')
if (nodesResult.success) {
stats.nodes = nodesResult.count || 0
}
// - 使Local Job (pagetype使API)
const taskQueueResult = await getLocalJobCount()
if (taskQueueResult.success) {
stats.taskQueue = taskQueueResult.count || 0
}
//
const scheduledTasksResult = await getCount('Local Scheduled Job')
if (scheduledTasksResult.success) {
stats.scheduledTasks = scheduledTasksResult.count || 0
}
// 5
//
const knowledgeBaseResult = await getCount('Knowledge Base')
if (knowledgeBaseResult.success) {
stats.knowledgeBase = knowledgeBaseResult.count || 0
}
//
const noteResult = await getCount('Note')
if (noteResult.success) {
stats.note = noteResult.count || 0
}
//
const eventResult = await getCount('Event')
if (eventResult.success) {
stats.event = eventResult.count || 0
}
//
const todoResult = await getCount('ToDo')
if (todoResult.success) {
stats.todo = todoResult.count || 0
}
//
const fileResult = await getCount('File')
if (fileResult.success) {
stats.file = fileResult.count || 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
onMounted(() => {
//
loadStats()
})
</script>
<style scoped>
.dashboard-page {
width: 100%;
padding: 0 16px;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 28px;
font-weight: 700;
color: #1f2937;
margin: 0 0 8px 0;
}
.page-description {
font-size: 16px;
color: #6b7280;
margin: 0;
}
.stats-grid {
margin-bottom: 24px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.stats-grid {
--n-grid-cols: 3;
}
}
@media (max-width: 768px) {
.dashboard-page {
padding: 0 12px;
}
.stats-grid {
margin-bottom: 16px;
--n-grid-cols: 2;
}
}
@media (max-width: 480px) {
.dashboard-page {
padding: 0 8px;
}
.page-title {
font-size: 24px;
}
.stats-grid {
--n-grid-cols: 1;
}
}
</style>

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) => {
@ -182,8 +182,6 @@ const handleSignupSubmit = async () => {
if (result.user) {
authStore.user = result.user
authStore.isAuthenticated = true
localStorage.setItem('jingrow_user', JSON.stringify(result.user))
localStorage.setItem('jingrow_authenticated', 'true')
showSignupModal.value = false
signupFormData.username = ''
signupFormData.password = ''
@ -207,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
@ -228,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) {
@ -244,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()
@ -291,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 ''
@ -311,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)
@ -455,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
@ -474,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'))
@ -482,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'))
@ -490,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')) {
@ -517,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)
@ -583,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())
@ -600,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 {
@ -622,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}`)
@ -640,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
@ -656,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) {
@ -674,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'))
}
}
@ -685,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 = ''
@ -711,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
@ -748,7 +739,7 @@ const adjustContainerSize = async () => {
}
}
//
// Handle single image view container
const singleWrapper = singleImageWrapperRef.value
if (singleWrapper) {
const img = singleImageRef.value
@ -789,27 +780,38 @@ const adjustContainerSize = async () => {
watch([uploadedImageUrl, resultImageUrl], adjustContainerSize, { immediate: true })
const handleSplitLineMouseDown = (e: MouseEvent) => {
const handleSplitLineMouseDown = (e: MouseEvent | TouchEvent) => {
e.preventDefault()
isDraggingSplitLine.value = true
const handleMouseMove = (moveEvent: MouseEvent) => {
const getClientX = (event: MouseEvent | TouchEvent): number => {
if ('touches' in event && event.touches.length > 0) {
return event.touches[0].clientX
}
return (event as MouseEvent).clientX
}
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
if (!comparisonContainerRef.value || !isDraggingSplitLine.value) return
const rect = comparisonContainerRef.value.getBoundingClientRect()
const x = moveEvent.clientX - rect.left
const x = getClientX(moveEvent) - rect.left
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
splitPosition.value = percentage
}
const handleMouseUp = () => {
const handleEnd = () => {
isDraggingSplitLine.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('mousemove', handleMove as EventListener)
document.removeEventListener('mouseup', handleEnd)
document.removeEventListener('touchmove', handleMove as EventListener)
document.removeEventListener('touchend', handleEnd)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.addEventListener('mousemove', handleMove as EventListener)
document.addEventListener('mouseup', handleEnd)
document.addEventListener('touchmove', handleMove as EventListener, { passive: false })
document.addEventListener('touchend', handleEnd)
}
const handlePaste = async (event: ClipboardEvent) => {
@ -908,13 +910,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
@ -922,12 +924,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')) {
@ -951,7 +951,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)
}
@ -988,11 +988,11 @@ onMounted(async () => {
window.addEventListener('resize', handleWindowResize)
window.addEventListener('paste', handlePaste)
//
// Initialize auth state
await authStore.initAuth()
//
// Detect mobile
checkIsMobile()
})
@ -1002,7 +1002,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)
}
@ -1011,7 +1011,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">
@ -1099,7 +1099,7 @@ onUnmounted(() => {
<div class="comparison-image result-image" :style="{ clipPath: `inset(0 0 0 ${splitPosition}%)` }">
<img ref="resultImageRef" :src="resultImageUrl" :alt="t('Background Removed')" @load="adjustContainerSize" />
</div>
<div class="split-line" :class="{ dragging: isDraggingSplitLine }" :style="{ left: `${splitPosition}%` }" @mousedown.prevent.stop="handleSplitLineMouseDown">
<div class="split-line" :class="{ dragging: isDraggingSplitLine }" :style="{ left: `${splitPosition}%` }" @mousedown.prevent.stop="handleSplitLineMouseDown" @touchstart.prevent.stop="handleSplitLineMouseDown">
<div class="split-line-handle">
<i class="fa fa-arrows-h"></i>
</div>
@ -1161,7 +1161,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">
@ -1190,7 +1190,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">
@ -1214,7 +1214,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" />
@ -1238,12 +1238,11 @@ onUnmounted(() => {
</div>
</div>
<!-- 右侧三列链接 -->
<!-- Right: three column links -->
<div class="footer-right">
<div class="footer-column">
<h3 class="footer-title">{{ t('Products & Services') }}</h3>
<ul class="footer-links">
<li><a href="https://jingrow.com" target="_blank">Jingrow</a></li>
<li><a href="https://jingrowtools.com" target="_blank">{{ t('Jingrow Tools') }}</a></li>
</ul>
</div>
@ -1271,10 +1270,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"
@ -1290,14 +1289,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">
@ -1366,7 +1365,7 @@ onUnmounted(() => {
<div class="comparison-image result-image" :style="{ clipPath: `inset(0 0 0 ${splitPosition}%)` }">
<img ref="resultImageRef" :src="resultImageUrl" :alt="t('Background Removed')" @load="adjustContainerSize" />
</div>
<div class="split-line" :class="{ dragging: isDraggingSplitLine }" :style="{ left: `${splitPosition}%` }" @mousedown.prevent.stop="handleSplitLineMouseDown">
<div class="split-line" :class="{ dragging: isDraggingSplitLine }" :style="{ left: `${splitPosition}%` }" @mousedown.prevent.stop="handleSplitLineMouseDown" @touchstart.prevent.stop="handleSplitLineMouseDown">
<div class="split-line-handle">
<i class="fa fa-arrows-h"></i>
</div>
@ -1428,7 +1427,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">
@ -1455,7 +1454,7 @@ onUnmounted(() => {
</n-layout-content>
</n-layout>
<!-- 移动端遮罩层 -->
<!-- Mobile overlay -->
<div
v-if="isMobile && !collapsed"
class="mobile-overlay"
@ -1464,7 +1463,7 @@ onUnmounted(() => {
</n-layout>
</template>
<!-- 登录弹窗 -->
<!-- Login modal -->
<n-modal
v-model:show="showLoginModal"
preset="card"
@ -1533,7 +1532,7 @@ onUnmounted(() => {
</div>
</n-modal>
<!-- 注册弹窗 -->
<!-- Signup modal -->
<n-modal
v-model:show="showSignupModal"
preset="card"
@ -1644,7 +1643,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;
@ -1652,7 +1651,7 @@ onUnmounted(() => {
overflow: hidden;
}
/* 应用布局样式(登录后) */
/* App layout styles (after login) */
.app-layout {
height: 100vh;
}
@ -1663,14 +1662,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;
@ -1681,9 +1680,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;
@ -1696,18 +1695,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;
@ -1969,7 +1968,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;
}
@ -1978,10 +1977,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;
@ -2101,7 +2100,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;
@ -2115,7 +2114,7 @@ onUnmounted(() => {
border: none;
box-shadow: none;
background: transparent;
min-height: 600px; /* 确保预览区域有足够高度 */
min-height: 600px; /* Ensure preview area has sufficient height */
height: auto;
}
@ -2342,8 +2341,6 @@ onUnmounted(() => {
img {
display: block;
min-width: 620px;
min-height: 620px;
max-width: 100%;
max-height: 100%;
width: auto;

View File

@ -230,8 +230,6 @@ const handleSignup = async () => {
if (result.user) {
authStore.user = result.user
authStore.isAuthenticated = true
localStorage.setItem('jingrow_user', JSON.stringify(result.user))
localStorage.setItem('jingrow_authenticated', 'true')
router.push('/')
} else {
const loginResult = await authStore.login(formData.username, formData.password)

File diff suppressed because it is too large Load Diff

View File

@ -117,6 +117,7 @@
:class="{ 'dragging': isDraggingSplitLine }"
:style="{ left: `${splitPosition}%` }"
@mousedown.prevent.stop="handleSplitLineMouseDown"
@touchstart.prevent.stop="handleSplitLineMouseDown"
>
<div class="split-line-handle">
<i class="fa fa-arrows-h"></i>
@ -218,11 +219,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 +283,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 +296,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 +352,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 +360,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 +368,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 +376,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 +391,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 +419,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 +496,7 @@ const processFile = async (file: File) => {
return
}
// 1024x1024
// Compress image to 1024x1024
try {
const compressedFile = await compressImageFile(file, {
maxWidth: 1024,
@ -518,17 +517,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 +556,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)
}
@ -571,27 +569,38 @@ const getHistoryThumbnailUrl = (item: HistoryItem): string => {
return item.originalImageUrl
}
const handleSplitLineMouseDown = (e: MouseEvent) => {
const handleSplitLineMouseDown = (e: MouseEvent | TouchEvent) => {
e.preventDefault()
isDraggingSplitLine.value = true
const handleMouseMove = (moveEvent: MouseEvent) => {
const getClientX = (event: MouseEvent | TouchEvent): number => {
if ('touches' in event && event.touches.length > 0) {
return event.touches[0].clientX
}
return (event as MouseEvent).clientX
}
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
if (!comparisonContainerRef.value || !isDraggingSplitLine.value) return
const rect = comparisonContainerRef.value.getBoundingClientRect()
const x = moveEvent.clientX - rect.left
const x = getClientX(moveEvent) - rect.left
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
splitPosition.value = percentage
}
const handleMouseUp = () => {
const handleEnd = () => {
isDraggingSplitLine.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('mousemove', handleMove as EventListener)
document.removeEventListener('mouseup', handleEnd)
document.removeEventListener('touchmove', handleMove as EventListener)
document.removeEventListener('touchend', handleEnd)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.addEventListener('mousemove', handleMove as EventListener)
document.addEventListener('mouseup', handleEnd)
document.addEventListener('touchmove', handleMove as EventListener, { passive: false })
document.addEventListener('touchend', handleEnd)
}
const handleRemoveBackground = async () => {
@ -608,7 +617,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 +683,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 +700,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 +721,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 +739,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 +747,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 +761,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 +770,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'))
}
}

View File

@ -16,7 +16,7 @@ export default defineConfig(({ mode, command }) => {
const BACKEND_URL = env.VITE_BACKEND_SERVER_URL || 'https://api.jingrow.com'
const JCLOUD_URL = env.VITE_JCLOUD_SERVER_URL || 'https://cloud.jingrow.com'
const FRONTEND_HOST = env.VITE_FRONTEND_HOST || '0.0.0.0'
const FRONTEND_PORT = Number(env.VITE_FRONTEND_PORT) || 3100
const FRONTEND_PORT = Number(env.VITE_FRONTEND_PORT) || 3001
const ALLOWED_HOSTS = (env.VITE_ALLOWED_HOSTS || '').split(',').map((s) => s.trim()).filter(Boolean)
return {
@ -68,12 +68,16 @@ export default defineConfig(({ mode, command }) => {
'/api/action': {
target: JCLOUD_URL,
changeOrigin: true,
secure: true,
secure: false,
cookieDomainRewrite: { '*': '' },
cookiePathRewrite: { '*': '/' },
},
'/api/data': {
target: JCLOUD_URL,
changeOrigin: true,
secure: true,
secure: false,
cookieDomainRewrite: { '*': '' },
cookiePathRewrite: { '*': '/' },
},
'/api/v1/tools': {
target: BACKEND_URL,