Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f092728a04 | |||
| 8820fc8e06 | |||
| de92a1e63d | |||
| c489d41941 | |||
| ee2f59df81 | |||
| 36af166879 | |||
| 6e6d822b06 | |||
| dfad1a1d80 | |||
| 423e28c09c | |||
| f30502bf7d | |||
| 79b92e7aae | |||
| 911ae5e53b | |||
| 4b3ebaa7ed | |||
| eb70a0c6f6 |
@ -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>
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
514
src/shared/api/account.ts
Normal 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 || '禁用双因素认证失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
src/shared/components/ClickToCopyField.vue
Normal file
79
src/shared/components/ClickToCopyField.vue
Normal 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>
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
|
||||
@ -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
@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user