fix: minapp search error

This commit is contained in:
kangfenmao 2025-05-23 13:52:46 +08:00
parent 1b125270b5
commit 446acbc662
6 changed files with 318 additions and 295 deletions

View File

@ -51,7 +51,6 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
)}
<App isLast app={minapps[0]} onClick={handleClose} size={50} />
</AppsContainer>
</PopoverContent>
)

View File

@ -100,7 +100,7 @@
"titleLabel": "标题",
"titlePlaceholder": "输入标题",
"contentLabel": "内容",
"contentPlaceholder": "请输入短语内容支持使用变量然后按Tab键可以快速定位到变量进行修改。比如\n帮我规划从${from}到${to}的路线,然后发送到${email}"
"contentPlaceholder": "请输入短语内容支持使用变量然后按Tab键可以快速定位到变量进行修改。比如\n帮我规划从${from}到${to}的路线,然后发送到${email}"
}
},
"auth": {
@ -113,7 +113,7 @@
"backup": {
"confirm": "确定要备份数据吗?",
"confirm.button": "选择备份位置",
"content": "备份全部数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待",
"content": "备份全部数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待",
"progress": {
"completed": "备份完成",
"compressing": "压缩文件...",
@ -144,7 +144,7 @@
"artifacts.preview.openExternal.error.content": "外部浏览器打开出错",
"assistant.search.placeholder": "搜索",
"deeply_thought": "已深度思考(用时 {{seconds}} 秒)",
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天",
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天",
"default.name": "默认助手",
"default.topic.name": "默认话题",
"history": {
@ -238,7 +238,7 @@
"settings.code_cacheable": "代码块缓存",
"settings.code_cacheable.tip": "缓存代码块可以减少长代码块的渲染时间,但会增加内存占用",
"settings.code_cache_max_size": "缓存上限",
"settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多",
"settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多",
"settings.code_cache_ttl": "缓存期限",
"settings.code_cache_ttl.tip": "缓存过期时间(分钟)",
"settings.code_cache_threshold": "缓存阈值",
@ -942,7 +942,7 @@
"restore": {
"confirm": "确定要恢复数据吗?",
"confirm.button": "选择备份文件",
"content": "恢复操作将使用备份数据覆盖当前所有应用数据。请注意,恢复过程可能需要一些时间,感谢您的耐心等待",
"content": "恢复操作将使用备份数据覆盖当前所有应用数据。请注意,恢复过程可能需要一些时间,感谢您的耐心等待",
"progress": {
"completed": "恢复完成",
"copying_files": "复制文件... {{progress}}%",
@ -995,7 +995,7 @@
"app_knowledge.remove_all_success": "文件删除成功",
"app_logs": "应用日志",
"backup.skip_file_data_title": "精简备份",
"backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度",
"backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度",
"clear_cache": {
"button": "清除缓存",
"confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?",
@ -1037,7 +1037,7 @@
"url": "Joplin 剪裁服务监听 URL",
"url_placeholder": "http://127.0.0.1:41184/"
},
"markdown_export.force_dollar_math.help": "开启后导出Markdown时会将强制使用$$来标记LaTeX公式。注意该项也会影响所有通过Markdown导出的方式如Notion、语雀等",
"markdown_export.force_dollar_math.help": "开启后导出Markdown时会将强制使用$$来标记LaTeX公式。注意该项也会影响所有通过Markdown导出的方式如Notion、语雀等",
"markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式",
"markdown_export.help": "若填入,则每次导出时将自动保存到该路径;否则,将弹出保存对话框",
"markdown_export.path": "默认导出路径",
@ -1045,7 +1045,7 @@
"markdown_export.select": "选择",
"markdown_export.title": "Markdown 导出",
"message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题",
"message_title.use_topic_naming.help": "开启后使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式",
"message_title.use_topic_naming.help": "开启后使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式",
"minute_interval_one": "{{count}} 分钟",
"minute_interval_other": "{{count}} 分钟",
"notion.api_key": "Notion 密钥",
@ -1084,8 +1084,8 @@
"backup.manager.restore.success": "恢复成功,应用将在几秒后刷新",
"backup.manager.restore.error": "恢复失败",
"backup.manager.delete.confirm.title": "确认删除",
"backup.manager.delete.confirm.single": "确定要删除备份文件 \"{{fileName}}\" 吗?此操作不可恢复",
"backup.manager.delete.confirm.multiple": "确定要删除选中的 {{count}} 个备份文件吗?此操作不可恢复",
"backup.manager.delete.confirm.single": "确定要删除备份文件 \"{{fileName}}\" 吗?此操作不可恢复",
"backup.manager.delete.confirm.multiple": "确定要删除选中的 {{count}} 个备份文件吗?此操作不可恢复",
"backup.manager.delete.success.single": "删除成功",
"backup.manager.delete.success.multiple": "成功删除 {{count}} 个备份文件",
"backup.manager.delete.error": "删除失败",
@ -1215,20 +1215,20 @@
"custom": {
"title": "自定义",
"edit_title": "编辑自定义小程序",
"save_success": "自定义小程序保存成功",
"save_error": "自定义小程序保存失败",
"remove_success": "自定义小程序删除成功",
"remove_error": "自定义小程序删除失败",
"logo_upload_success": "Logo 上传成功",
"logo_upload_error": "Logo 上传失败",
"save_success": "自定义小程序保存成功",
"save_error": "自定义小程序保存失败",
"remove_success": "自定义小程序删除成功",
"remove_error": "自定义小程序删除失败",
"logo_upload_success": "Logo 上传成功",
"logo_upload_error": "Logo 上传失败",
"id": "ID",
"id_error": "ID 是必填项",
"id_error": "ID 是必填项",
"id_placeholder": "请输入 ID",
"name": "名称",
"name_error": "名称是必填项",
"name_error": "名称是必填项",
"name_placeholder": "请输入名称",
"url": "URL",
"url_error": "URL 是必填项",
"url_error": "URL 是必填项",
"url_placeholder": "请输入 URL",
"logo": "Logo",
"logo_url": "Logo URL",
@ -1238,7 +1238,7 @@
"logo_upload_label": "上传 Logo",
"logo_upload_button": "上传",
"save": "保存",
"edit_description": "在这里编辑自定义小应用的配置。每个应用需要包含 id、name、url 和 logo 字段",
"edit_description": "在这里编辑自定义小应用的配置。每个应用需要包含 id、name、url 和 logo 字段",
"placeholder": "请输入自定义小程序配置JSON格式",
"duplicate_ids": "发现重复的ID: {{ids}}",
"conflicting_ids": "与默认应用ID冲突: {{ids}}"
@ -1286,7 +1286,7 @@
"addServer": "添加服务器",
"addServer.create": "快速创建",
"addServer.importFrom": "从 JSON 导入",
"addServer.importFrom.tooltip": "请从 MCP Servers 的介绍页面复制配置JSON优先使用\n NPX或 UVX 配置),并粘贴到输入框中",
"addServer.importFrom.tooltip": "请从 MCP Servers 的介绍页面复制配置JSON优先使用\n NPX或 UVX 配置),并粘贴到输入框中",
"addServer.importFrom.placeholder": "粘贴 MCP 服务器 JSON 配置",
"addServer.importFrom.invalid": "无效输入,请检查 JSON 格式",
"addServer.importFrom.nameExists": "服务器已存在:{{name}}",
@ -1321,7 +1321,7 @@
"installError": "安装依赖项失败",
"installSuccess": "依赖项安装成功",
"jsonFormatError": "JSON格式化错误",
"jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确",
"jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确",
"jsonSaveError": "保存JSON配置失败",
"jsonSaveSuccess": "JSON配置已保存",
"missingDependencies": "缺失,请安装它以继续",
@ -1388,7 +1388,7 @@
"deleteServer": "删除服务器",
"deleteServerConfirm": "确定要删除此服务器吗?",
"registry": "包管理源",
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题",
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题",
"registryDefault": "默认",
"not_support": "模型不支持",
"user": "用户",
@ -1472,7 +1472,7 @@
"models.check.model_status_partial": "其中 {{count}} 个模型用某些密钥无法访问",
"models.check.model_status_passed": "{{count}} 个模型通过健康检测",
"models.check.model_status_summary": "{{provider}}: {{summary}}",
"models.check.no_api_keys": "未找到API密钥请先添加API密钥",
"models.check.no_api_keys": "未找到API密钥请先添加API密钥",
"models.check.passed": "通过",
"models.check.select_api_key": "选择要使用的API密钥",
"models.check.single": "单个",
@ -1520,7 +1520,7 @@
"api_key.tip": "多个密钥使用逗号分隔",
"api_version": "API 版本",
"basic_auth": "HTTP 认证",
"basic_auth.tip": "适用于通过服务器部署的实例(参见文档)。目前仅支持 Basic 方案RFC7617",
"basic_auth.tip": "适用于通过服务器部署的实例(参见文档)。目前仅支持 Basic 方案RFC7617",
"basic_auth.user_name": "用户名",
"basic_auth.user_name.tip": "留空以禁用",
"basic_auth.password": "密码",
@ -1682,7 +1682,7 @@
"titleLabel": "标题",
"contentLabel": "内容",
"titlePlaceholder": "请输入短语标题",
"contentPlaceholder": "请输入短语内容支持使用变量然后按Tab键可以快速定位到变量进行修改。比如\n帮我规划从${from}到${to}的路线,然后发送到${email}",
"contentPlaceholder": "请输入短语内容支持使用变量然后按Tab键可以快速定位到变量进行修改。比如\n帮我规划从${from}到${to}的路线,然后发送到${email}",
"delete": "删除短语",
"deleteConfirm": "删除短语后将无法恢复,是否继续?",
"locationLabel": "添加位置",

View File

@ -93,7 +93,7 @@
"titleLabel": "標題",
"titlePlaceholder": "輸入標題",
"contentLabel": "內容",
"contentPlaceholder": "請輸入短語內容支持使用變量然後按Tab鍵可以快速定位到變量進行修改。比如\n幫我規劃從${from}到${to}的行程,然後發送到${email}"
"contentPlaceholder": "請輸入短語內容支持使用變量然後按Tab鍵可以快速定位到變量進行修改。比如\n幫我規劃從${from}到${to}的行程,然後發送到${email}"
},
"settings.knowledge_base.recognition.tip": "智慧代理人將調用大語言模型的意圖識別能力,判斷是否需要調用知識庫進行回答,該功能將依賴模型的能力",
"settings.knowledge_base.recognition": "調用知識庫",
@ -113,7 +113,7 @@
"backup": {
"confirm": "確定要備份資料嗎?",
"confirm.button": "選擇備份位置",
"content": "備份全部資料,包括聊天記錄、設定、知識庫等全部資料。請注意,備份過程可能需要一些時間,感謝您的耐心等待",
"content": "備份全部資料,包括聊天記錄、設定、知識庫等全部資料。請注意,備份過程可能需要一些時間,感謝您的耐心等待",
"progress": {
"completed": "備份完成",
"compressing": "壓縮檔案...",
@ -144,7 +144,7 @@
"artifacts.preview.openExternal.error.content": "外部瀏覽器開啟出錯",
"assistant.search.placeholder": "搜尋",
"deeply_thought": "已深度思考(用時 {{seconds}} 秒)",
"default.description": "你好,我是預設助手。你可以立即開始與我聊天",
"default.description": "你好,我是預設助手。你可以立即開始與我聊天",
"default.name": "預設助手",
"default.topic.name": "預設話題",
"history": {
@ -224,18 +224,18 @@
"settings.code_cacheable": "程式碼區塊快取",
"settings.code_cacheable.tip": "快取程式碼區塊可以減少長程式碼區塊的渲染時間,但會增加記憶體使用量",
"settings.code_cache_max_size": "快取上限",
"settings.code_cache_max_size.tip": "允許快取的字元數上限(千字符),按照高亮後的程式碼計算。高亮後的程式碼長度相比純文字會長很多",
"settings.code_cache_max_size.tip": "允許快取的字元數上限(千字符),按照高亮後的程式碼計算。高亮後的程式碼長度相比純文字會長很多",
"settings.code_cache_ttl": "快取期限",
"settings.code_cache_ttl.tip": "快取的存活時間(分鐘)",
"settings.code_cache_threshold": "快取門檻",
"settings.code_cache_threshold.tip": "允許快取的最小程式碼長度(千字符),超過門檻的程式碼區塊才會被快取",
"settings.context_count": "上下文",
"settings.context_count.tip": "在上下文中保留的前幾則訊息",
"settings.context_count.tip": "在上下文中保留的前幾則訊息",
"settings.max": "最大",
"settings.max_tokens": "最大 Token 數",
"settings.max_tokens.confirm": "設置最大 Token 數",
"settings.max_tokens.confirm_content": "設置單次交互所用的最大 Token 數,會影響返回結果的長度。要根據模型上下文限制來設定,否則會發生錯誤",
"settings.max_tokens.tip": "模型可以生成的最大 Token 數。要根據模型上下文限制來設定,否則會發生錯誤",
"settings.max_tokens.confirm_content": "設置單次交互所用的最大 Token 數,會影響返回結果的長度。要根據模型上下文限制來設定,否則會發生錯誤",
"settings.max_tokens.tip": "模型可以生成的最大 Token 數。要根據模型上下文限制來設定,否則會發生錯誤",
"settings.reset": "重設",
"settings.set_as_default": "設為預設助手",
"settings.show_line_numbers": "程式碼顯示行號",
@ -467,7 +467,7 @@
"type": "類型"
},
"gpustack": {
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.placeholder": "分鐘",
"keep_alive_time.title": "保持活躍時間",
"title": "GPUStack"
@ -568,7 +568,7 @@
"spanish": "西班牙文"
},
"lmstudio": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.placeholder": "分鐘",
"keep_alive_time.title": "保持活躍時間",
"title": "LM Studio"
@ -788,7 +788,7 @@
"knowledge.error": "無法將 {{type}} 加入知識庫: {{error}}"
},
"ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.placeholder": "分鐘",
"keep_alive_time.title": "保持活躍時間",
"title": "Ollama"
@ -943,7 +943,7 @@
"restore": {
"confirm": "確定要復原資料嗎?",
"confirm.button": "選擇備份檔案",
"content": "復原操作將使用備份資料覆蓋目前所有應用程式資料。請注意,復原過程可能需要一些時間,感謝您的耐心等待",
"content": "復原操作將使用備份資料覆蓋目前所有應用程式資料。請注意,復原過程可能需要一些時間,感謝您的耐心等待",
"progress": {
"completed": "復原完成",
"copying_files": "複製檔案... {{progress}}%",
@ -996,7 +996,7 @@
"app_knowledge.remove_all_success": "檔案刪除成功",
"app_logs": "應用程式日誌",
"backup.skip_file_data_title": "精簡備份",
"backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度",
"backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度",
"clear_cache": {
"button": "清除快取",
"confirm": "清除快取將刪除應用快取資料,包括小工具資料。此操作不可恢復,是否繼續?",
@ -1038,9 +1038,9 @@
"url": "Joplin 剪輯服務 URL",
"url_placeholder": "http://127.0.0.1:41184/"
},
"markdown_export.force_dollar_math.help": "開啟後匯出Markdown時會強制使用$$來標記LaTeX公式。注意該項也會影響所有透過Markdown匯出的方式如Notion、語雀等",
"markdown_export.force_dollar_math.help": "開啟後匯出Markdown時會強制使用$$來標記LaTeX公式。注意該項也會影響所有透過Markdown匯出的方式如Notion、語雀等",
"markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$",
"markdown_export.help": "若填入,每次匯出時將自動儲存至該路徑;否則,將彈出儲存對話框",
"markdown_export.help": "若填入,每次匯出時將自動儲存至該路徑;否則,將彈出儲存對話框",
"markdown_export.path": "預設匯出路徑",
"markdown_export.path_placeholder": "匯出路徑",
"markdown_export.select": "選擇",
@ -1083,8 +1083,8 @@
"backup.manager.restore.success": "恢復成功,應用將在幾秒後刷新",
"backup.manager.restore.error": "恢復失敗",
"backup.manager.delete.confirm.title": "確認刪除",
"backup.manager.delete.confirm.single": "確定要刪除備份文件 \"{{fileName}}\" 嗎?此操作不可恢復",
"backup.manager.delete.confirm.multiple": "確定要刪除選中的 {{count}} 個備份文件嗎?此操作不可恢復",
"backup.manager.delete.confirm.single": "確定要刪除備份文件 \"{{fileName}}\" 嗎?此操作不可恢復",
"backup.manager.delete.confirm.multiple": "確定要刪除選中的 {{count}} 個備份文件嗎?此操作不可恢復",
"backup.manager.delete.success.single": "刪除成功",
"backup.manager.delete.success.multiple": "成功刪除 {{count}} 個備份文件",
"backup.manager.delete.error": "刪除失敗",
@ -1187,7 +1187,7 @@
"new_folder.button": "新建文件夾"
},
"message_title.use_topic_naming.title": "使用話題命名模型為導出的消息創建標題",
"message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式如Notion、語雀等"
"message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式如Notion、語雀等"
},
"display.assistant.title": "助手設定",
"display.custom.css": "自訂 CSS",
@ -1218,20 +1218,20 @@
"conflicting_ids": "與預設應用ID衝突: {{ids}}",
"title": "自定義",
"edit_title": "編輯自定義小程序",
"save_success": "自定義小程序保存成功",
"save_error": "自定義小程序保存失敗",
"remove_success": "自定義小程序刪除成功",
"remove_error": "自定義小程序刪除失敗",
"logo_upload_success": "Logo 上傳成功",
"logo_upload_error": "Logo 上傳失敗",
"save_success": "自定義小程序保存成功",
"save_error": "自定義小程序保存失敗",
"remove_success": "自定義小程序刪除成功",
"remove_error": "自定義小程序刪除失敗",
"logo_upload_success": "Logo 上傳成功",
"logo_upload_error": "Logo 上傳失敗",
"id": "ID",
"id_error": "ID 是必填項",
"id_error": "ID 是必填項",
"id_placeholder": "請輸入 ID",
"name": "名稱",
"name_error": "名稱是必填項",
"name_error": "名稱是必填項",
"name_placeholder": "請輸入名稱",
"url": "URL",
"url_error": "URL 是必填項",
"url_error": "URL 是必填項",
"url_placeholder": "請輸入 URL",
"logo": "Logo",
"logo_url": "Logo URL",
@ -1286,7 +1286,7 @@
"addServer": "新增伺服器",
"addServer.create": "快速創建",
"addServer.importFrom": "從 JSON 導入",
"addServer.importFrom.tooltip": "請從 MCP Servers 的介紹頁面複製配置JSON優先使用\n NPX或 UVX 配置),並粘貼到輸入框中",
"addServer.importFrom.tooltip": "請從 MCP Servers 的介紹頁面複製配置JSON優先使用\n NPX或 UVX 配置),並粘貼到輸入框中",
"addServer.importFrom.placeholder": "貼上 MCP 伺服器 JSON 設定",
"addServer.importFrom.invalid": "無效的輸入,請檢查 JSON 格式",
"addServer.importFrom.nameExists": "伺服器已存在:{{name}}",
@ -1321,7 +1321,7 @@
"installError": "安裝相依套件失敗",
"installSuccess": "相依套件安裝成功",
"jsonFormatError": "JSON格式錯誤",
"jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確",
"jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確",
"jsonSaveError": "保存JSON配置失敗",
"jsonSaveSuccess": "JSON配置已儲存",
"missingDependencies": "缺失,請安裝它以繼續",
@ -1388,7 +1388,7 @@
"deleteServer": "刪除伺服器",
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
"registry": "套件管理源",
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題",
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題",
"registryDefault": "預設",
"not_support": "不支援此模型",
"user": "用戶",
@ -1472,7 +1472,7 @@
"models.check.model_status_partial": "其中 {{count}} 個模型用某些密鑰無法訪問",
"models.check.model_status_passed": "{{count}} 個模型通過健康檢查",
"models.check.model_status_summary": "{{provider}}: {{summary}}",
"models.check.no_api_keys": "未找到API密鑰請先添加API密鑰",
"models.check.no_api_keys": "未找到API密鑰請先添加API密鑰",
"models.check.passed": "通過",
"models.check.select_api_key": "選擇要使用的API密鑰",
"models.check.single": "單個",
@ -1514,7 +1514,7 @@
"api_key.tip": "多個金鑰使用逗號分隔",
"api_version": "API 版本",
"basic_auth": "HTTP 認證",
"basic_auth.tip": "適用於透過伺服器部署的實例(請參閱文檔)。目前僅支援 Basic 方案RFC7617",
"basic_auth.tip": "適用於透過伺服器部署的實例(請參閱文檔)。目前僅支援 Basic 方案RFC7617",
"basic_auth.user_name": "用戶",
"basic_auth.user_name.tip": "留空以停用",
"basic_auth.password": "密碼",
@ -1572,7 +1572,7 @@
"markdown_editor_default_value": "預覽區域"
},
"openai": {
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API請建立新的服務供應商"
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API請建立新的服務供應商"
}
},
"proxy": {
@ -1667,7 +1667,7 @@
"apikey": "API 金鑰",
"free": "免費",
"content_limit": "內容長度限制",
"content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷"
"content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷"
},
"general.auto_check_update.title": "啟用自動更新",
"quickPhrase": {
@ -1677,7 +1677,7 @@
"titleLabel": "標題",
"contentLabel": "內容",
"titlePlaceholder": "請輸入短語標題",
"contentPlaceholder": "請輸入短語內容支持使用變量然後按Tab鍵可以快速定位到變量進行修改。比如\n幫我規劃從${from}到${to}的行程,然後發送到${email}",
"contentPlaceholder": "請輸入短語內容支持使用變量然後按Tab鍵可以快速定位到變量進行修改。比如\n幫我規劃從${from}到${to}的行程,然後發送到${email}",
"delete": "刪除短語",
"deleteConfirm": "刪除後無法復原,是否繼續?",
"locationLabel": "添加位置",

View File

@ -1,13 +1,11 @@
import { PlusOutlined, UploadOutlined } from '@ant-design/icons'
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Button, Dropdown, Form, Input, message, Modal, Radio, Upload } from 'antd'
import type { UploadFile } from 'antd/es/upload/interface'
import { FC, useState } from 'react'
import { Dropdown, message } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -24,215 +22,72 @@ const App: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
const [isModalVisible, setIsModalVisible] = useState(false)
const [form] = Form.useForm()
const [logoType, setLogoType] = useState<'url' | 'file'>('url')
const [fileList, setFileList] = useState<UploadFile[]>([])
const handleClick = () => {
if (isLast) {
setIsModalVisible(true)
return
}
openMinappKeepAlive(app)
onClick?.()
}
const handleAddCustomApp = async (values: any) => {
try {
const content = await window.api.file.read('custom-minapps.json')
const customApps = JSON.parse(content)
// Check for duplicate ID
if (customApps.some((app: MinAppType) => app.id === values.id)) {
message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id }))
return
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
}
if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) {
message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id }))
return
},
{
key: 'hide',
label: t('minapp.sidebar.hide.title'),
onClick: () => {
const newMinapps = minapps.filter((item) => item.id !== app.id)
updateMinapps(newMinapps)
const newDisabled = [...(disabled || []), app]
updateDisabledMinapps(newDisabled)
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
}
const newApp: MinAppType = {
id: values.id,
name: values.name,
url: values.url,
logo: form.getFieldValue('logo') || '',
type: 'Custom',
addTime: new Date().toISOString()
}
customApps.push(newApp)
await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2))
message.success(t('settings.miniapps.custom.save_success'))
setIsModalVisible(false)
form.resetFields()
setFileList([])
// 重新加载应用列表
const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
updateDefaultMinApps(reloadedApps)
updateMinapps([...minapps, newApp])
} catch (error) {
message.error(t('settings.miniapps.custom.save_error'))
console.error('Failed to save custom mini app:', error)
}
}
const handleLogoTypeChange = (e: any) => {
setLogoType(e.target.value)
form.setFieldValue('logo', '')
setFileList([])
}
const handleFileChange = async (info: any) => {
const file = info.fileList[info.fileList.length - 1]?.originFileObj
setFileList(info.fileList.slice(-1))
if (file) {
try {
const reader = new FileReader()
reader.onload = (event) => {
const base64Data = event.target?.result
if (typeof base64Data === 'string') {
message.success(t('settings.miniapps.custom.logo_upload_success'))
form.setFieldValue('logo', base64Data)
}
}
reader.readAsDataURL(file)
} catch (error) {
console.error('Failed to read file:', error)
message.error(t('settings.miniapps.custom.logo_upload_error'))
}
}
}
const menuItems: MenuProps['items'] = isLast
? []
: [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
}
},
{
key: 'hide',
label: t('minapp.sidebar.hide.title'),
onClick: () => {
const newMinapps = minapps.filter((item) => item.id !== app.id)
updateMinapps(newMinapps)
const newDisabled = [...(disabled || []), app]
updateDisabledMinapps(newDisabled)
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
}
},
...(app.type === 'Custom'
? [
{
key: 'removeCustom',
label: t('minapp.sidebar.remove_custom.title'),
danger: true,
onClick: async () => {
try {
const content = await window.api.file.read('custom-minapps.json')
const customApps = JSON.parse(content)
const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id)
await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2))
message.success(t('settings.miniapps.custom.remove_success'))
const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
updateDefaultMinApps(reloadedApps)
updateMinapps(minapps.filter((item) => item.id !== app.id))
updatePinnedMinapps(pinned.filter((item) => item.id !== app.id))
updateDisabledMinapps(disabled.filter((item) => item.id !== app.id))
} catch (error) {
message.error(t('settings.miniapps.custom.remove_error'))
console.error('Failed to remove custom mini app:', error)
}
}
},
...(app.type === 'Custom'
? [
{
key: 'removeCustom',
label: t('minapp.sidebar.remove_custom.title'),
danger: true,
onClick: async () => {
try {
const content = await window.api.file.read('custom-minapps.json')
const customApps = JSON.parse(content)
const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id)
await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2))
message.success(t('settings.miniapps.custom.remove_success'))
const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
updateDefaultMinApps(reloadedApps)
updateMinapps(minapps.filter((item) => item.id !== app.id))
updatePinnedMinapps(pinned.filter((item) => item.id !== app.id))
updateDisabledMinapps(disabled.filter((item) => item.id !== app.id))
} catch (error) {
message.error(t('settings.miniapps.custom.remove_error'))
console.error('Failed to remove custom mini app:', error)
}
]
: [])
]
}
}
]
: [])
]
if (!isVisible && !isLast) {
if (!isVisible) {
return null
}
return (
<>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleClick}>
{isLast ? (
<AddButton size={size}>
<PlusOutlined />
</AddButton>
) : (
<MinAppIcon size={size} app={app} />
)}
<AppTitle>{isLast ? t('settings.miniapps.custom.title') : app.name}</AppTitle>
</Container>
</Dropdown>
<Modal
title={t('settings.miniapps.custom.edit_title')}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false)
setFileList([])
}}
footer={null}
transitionName="animation-move-down"
centered>
<Form form={form} onFinish={handleAddCustomApp} layout="vertical">
<Form.Item
name="id"
label={t('settings.miniapps.custom.id')}
rules={[{ required: true, message: t('settings.miniapps.custom.id_error') }]}>
<Input placeholder={t('settings.miniapps.custom.id_placeholder')} />
</Form.Item>
<Form.Item
name="name"
label={t('settings.miniapps.custom.name')}
rules={[{ required: true, message: t('settings.miniapps.custom.name_error') }]}>
<Input placeholder={t('settings.miniapps.custom.name_placeholder')} />
</Form.Item>
<Form.Item
name="url"
label={t('settings.miniapps.custom.url')}
rules={[{ required: true, message: t('settings.miniapps.custom.url_error') }]}>
<Input placeholder={t('settings.miniapps.custom.url_placeholder')} />
</Form.Item>
<Form.Item label={t('settings.miniapps.custom.logo')}>
<Radio.Group value={logoType} onChange={handleLogoTypeChange}>
<Radio value="url">{t('settings.miniapps.custom.logo_url')}</Radio>
<Radio value="file">{t('settings.miniapps.custom.logo_file')}</Radio>
</Radio.Group>
</Form.Item>
{logoType === 'url' ? (
<Form.Item name="logo" label={t('settings.miniapps.custom.logo_url_label')}>
<Input placeholder={t('settings.miniapps.custom.logo_url_placeholder')} />
</Form.Item>
) : (
<Form.Item label={t('settings.miniapps.custom.logo_upload_label')}>
<Upload
accept="image/*"
maxCount={1}
fileList={fileList}
onChange={handleFileChange}
beforeUpload={() => false}>
<Button icon={<UploadOutlined />}>{t('settings.miniapps.custom.logo_upload_button')}</Button>
</Upload>
</Form.Item>
)}
<Form.Item>
<Button type="primary" htmlType="submit">
{t('settings.miniapps.custom.save')}
</Button>
</Form.Item>
</Form>
</Modal>
</>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleClick}>
<MinAppIcon size={size} app={app} />
<AppTitle>{isLast ? t('settings.miniapps.custom.title') : app.name}</AppTitle>
</Container>
</Dropdown>
)
}
@ -254,25 +109,4 @@ const AppTitle = styled.div`
white-space: nowrap;
`
const AddButton = styled.div<{ size?: number }>`
width: ${({ size }) => size || 60}px;
height: ${({ size }) => size || 60}px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-background-soft);
border: 1px dashed var(--color-border);
color: var(--color-text-soft);
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--color-background);
border-color: var(--color-primary);
color: var(--color-primary);
}
`
export default App

View File

@ -1,14 +1,13 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { Input } from 'antd'
import { isEmpty } from 'lodash'
import { Search } from 'lucide-react'
import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import App from './App'
import NewAppButton from './NewAppButton'
const AppsPage: FC = () => {
const { t } = useTranslation()
@ -51,18 +50,12 @@ const AppsPage: FC = () => {
</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
{isEmpty(filteredApps) ? (
<Center>
<App isLast app={filteredApps[0]} />
</Center>
) : (
<AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
<App isLast app={filteredApps[0]} />
</AppsContainer>
)}
<AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
<NewAppButton />
</AppsContainer>
</ContentContainer>
</Container>
)

View File

@ -0,0 +1,197 @@
import { PlusOutlined, UploadOutlined } from '@ant-design/icons'
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import { Button, Form, Input, message, Modal, Radio, Upload } from 'antd'
import type { UploadFile } from 'antd/es/upload/interface'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
size?: number
}
const NewAppButton: FC<Props> = ({ size = 60 }) => {
const { t } = useTranslation()
const [isModalVisible, setIsModalVisible] = useState(false)
const [fileList, setFileList] = useState<UploadFile[]>([])
const [logoType, setLogoType] = useState<'url' | 'file'>('url')
const [form] = Form.useForm()
const { minapps, updateMinapps } = useMinapps()
const handleLogoTypeChange = (e: any) => {
setLogoType(e.target.value)
form.setFieldValue('logo', '')
setFileList([])
}
const handleAddCustomApp = async (values: any) => {
try {
const content = await window.api.file.read('custom-minapps.json')
const customApps = JSON.parse(content)
// Check for duplicate ID
if (customApps.some((app: MinAppType) => app.id === values.id)) {
message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id }))
return
}
if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) {
message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id }))
return
}
const newApp: MinAppType = {
id: values.id,
name: values.name,
url: values.url,
logo: form.getFieldValue('logo') || '',
type: 'Custom',
addTime: new Date().toISOString()
}
customApps.push(newApp)
await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2))
message.success(t('settings.miniapps.custom.save_success'))
setIsModalVisible(false)
form.resetFields()
setFileList([])
const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
updateDefaultMinApps(reloadedApps)
updateMinapps([...minapps, newApp])
} catch (error) {
message.error(t('settings.miniapps.custom.save_error'))
console.error('Failed to save custom mini app:', error)
}
}
const handleFileChange = async (info: any) => {
const file = info.fileList[info.fileList.length - 1]?.originFileObj
setFileList(info.fileList.slice(-1))
if (file) {
try {
const reader = new FileReader()
reader.onload = (event) => {
const base64Data = event.target?.result
if (typeof base64Data === 'string') {
message.success(t('settings.miniapps.custom.logo_upload_success'))
form.setFieldValue('logo', base64Data)
}
}
reader.readAsDataURL(file)
} catch (error) {
console.error('Failed to read file:', error)
message.error(t('settings.miniapps.custom.logo_upload_error'))
}
}
}
return (
<>
<Container onClick={() => setIsModalVisible(true)}>
<AddButton size={size}>
<PlusOutlined />
</AddButton>
<AppTitle>{t('settings.miniapps.custom.title')}</AppTitle>
</Container>
<Modal
title={t('settings.miniapps.custom.edit_title')}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false)
setFileList([])
}}
footer={null}
transitionName="animation-move-down"
centered>
<Form form={form} onFinish={handleAddCustomApp} layout="vertical">
<Form.Item
name="id"
label={t('settings.miniapps.custom.id')}
rules={[{ required: true, message: t('settings.miniapps.custom.id_error') }]}>
<Input placeholder={t('settings.miniapps.custom.id_placeholder')} />
</Form.Item>
<Form.Item
name="name"
label={t('settings.miniapps.custom.name')}
rules={[{ required: true, message: t('settings.miniapps.custom.name_error') }]}>
<Input placeholder={t('settings.miniapps.custom.name_placeholder')} />
</Form.Item>
<Form.Item
name="url"
label={t('settings.miniapps.custom.url')}
rules={[{ required: true, message: t('settings.miniapps.custom.url_error') }]}>
<Input placeholder={t('settings.miniapps.custom.url_placeholder')} />
</Form.Item>
<Form.Item label={t('settings.miniapps.custom.logo')}>
<Radio.Group value={logoType} onChange={handleLogoTypeChange}>
<Radio value="url">{t('settings.miniapps.custom.logo_url')}</Radio>
<Radio value="file">{t('settings.miniapps.custom.logo_file')}</Radio>
</Radio.Group>
</Form.Item>
{logoType === 'url' ? (
<Form.Item name="logo" label={t('settings.miniapps.custom.logo_url_label')}>
<Input placeholder={t('settings.miniapps.custom.logo_url_placeholder')} />
</Form.Item>
) : (
<Form.Item label={t('settings.miniapps.custom.logo_upload_label')}>
<Upload
accept="image/*"
maxCount={1}
fileList={fileList}
onChange={handleFileChange}
beforeUpload={() => false}>
<Button icon={<UploadOutlined />}>{t('settings.miniapps.custom.logo_upload_button')}</Button>
</Upload>
</Form.Item>
)}
<Form.Item>
<Button type="primary" htmlType="submit">
{t('settings.miniapps.custom.save')}
</Button>
</Form.Item>
</Form>
</Modal>
</>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
`
const AddButton = styled.div<{ size?: number }>`
width: ${({ size }) => size || 60}px;
height: ${({ size }) => size || 60}px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-background-soft);
border: 1px dashed var(--color-border);
color: var(--color-text-soft);
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--color-background);
border-color: var(--color-primary);
color: var(--color-primary);
}
`
const AppTitle = styled.div`
font-size: 12px;
margin-top: 5px;
color: var(--color-text-soft);
text-align: center;
user-select: none;
white-space: nowrap;
`
export default NewAppButton