Skip to content

Commit ae6a4ad

Browse files
committed
feat: 优化 WebDAV 同步、生图模型测试、备份恢复功能
- 修复 WebDAV 同步描述文案,移除插件字样 - 添加 WebDAV 自动同步选项(定时同步、启动同步、保存时同步) - 使用自定义 Select 组件替换原生下拉框 - 优化生图模型测试,移除不兼容的参数 - 添加生图测试结果弹窗,显示生成的图片 - 添加生图 API 端点预览 - 备份/恢复功能支持导出图片和 AI 配置 - 简化私密文件夹设置提示 - 修复 AI 测试双弹窗问题 - 生图模型支持独立设置默认模型 - 完善多语言翻译(图片上传相关) - 更新 README 功能特性
1 parent 339c285 commit ae6a4ad

16 files changed

Lines changed: 1008 additions & 273 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,19 @@
4141
- **🔧 变量系统** - 模板变量 `{{variable}}`,动态替换
4242
- **📋 一键复制** - 快速复制 Prompt 到剪贴板
4343
- **🔍 全文搜索** - 快速搜索标题、描述和内容
44-
- **📤 数据导出** - JSON 格式备份和恢复
44+
- **📤 数据导出** - JSON 格式备份和恢复(包含图片和 AI 配置)
4545
- **🎨 主题定制** - 深色/浅色/跟随系统,多种主题色可选
4646
- **🌐 多语言** - 支持中文和英文界面
4747
- **💾 本地存储** - 所有数据存储在本地,隐私安全有保障
4848
- **🖥️ 跨平台** - 支持 macOS、Windows、Linux
4949
- **📊 列表视图** - 表格式展示 Prompt,支持排序和批量操作
5050
- **🤖 AI 测试** - 内置多模型测试,支持 18+ 服务商
51-
- **🧭 Markdown 预览** - 全场景支持 Markdown 渲染与代码高亮(忽略未知语言)
51+
- **🎨 生图模型** - 支持配置和测试图像生成模型(DALL-E、Midjourney 等)
52+
- **🧭 Markdown 预览** - 全场景支持 Markdown 渲染与代码高亮
5253
- **🪟 宽屏与全屏模式** - 编辑/查看详情时支持更宽的视野和全屏模式
53-
- **🔐 主密码与私密文件夹** - 支持设置主密码,锁定/解锁私密文件夹内容(加密存储开发中)
54+
- **🔐 主密码与私密文件夹** - 支持设置主密码,私密文件夹内容加密存储
5455
- **🖼️ 图片上传与预览** - 支持上传/粘贴本地图片,并在弹窗内预览
56+
- **☁️ WebDAV 同步** - 支持 WebDAV 云同步,自动定时同步、启动同步
5557

5658
## 📸 截图
5759

src/renderer/App.tsx

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,42 +56,82 @@ function App() {
5656
}
5757

5858
// 初始化数据库,然后加载数据
59-
const init = async () => {
59+
const init = async (retryCount = 0) => {
6060
try {
6161
await initDatabase();
6262
await seedDatabase();
63-
64-
// 检查是否需要自动同步(双向同步)
65-
const settings = useSettingsStore.getState();
66-
if (settings.webdavEnabled && settings.webdavAutoSync &&
67-
settings.webdavUrl && settings.webdavUsername && settings.webdavPassword) {
68-
console.log('🔄 Auto syncing with WebDAV (bidirectional)...');
63+
await fetchPrompts();
64+
await fetchFolders();
65+
console.log('✅ App initialized');
66+
} catch (error) {
67+
console.error('❌ Init failed:', error);
68+
// 如果是超时错误,尝试重试一次
69+
if (retryCount < 1 && error instanceof Error && error.message.includes('timeout')) {
70+
console.log('🔄 Retrying database initialization...');
71+
await new Promise(resolve => setTimeout(resolve, 500));
72+
return init(retryCount + 1);
73+
}
74+
} finally {
75+
setIsLoading(false);
76+
}
77+
78+
// 启动后同步(在数据加载完成后执行,不阻塞 UI)
79+
const settings = useSettingsStore.getState();
80+
if (settings.webdavEnabled && settings.webdavSyncOnStartup &&
81+
settings.webdavUrl && settings.webdavUsername && settings.webdavPassword) {
82+
const delay = (settings.webdavSyncOnStartupDelay || 10) * 1000;
83+
console.log(`🔄 Will sync with WebDAV in ${delay / 1000}s...`);
84+
setTimeout(async () => {
6985
try {
7086
const result = await autoSync({
7187
url: settings.webdavUrl,
7288
username: settings.webdavUsername,
7389
password: settings.webdavPassword,
7490
});
7591
if (result.success) {
76-
console.log('✅ Auto sync completed:', result.message);
92+
console.log('✅ Startup sync completed:', result.message);
93+
// 同步后重新加载数据
94+
await fetchPrompts();
95+
await fetchFolders();
7796
} else {
78-
console.log('⚠️ Auto sync failed:', result.message);
97+
console.log('⚠️ Startup sync failed:', result.message);
7998
}
8099
} catch (syncError) {
81-
console.error('⚠️ Auto sync error:', syncError);
100+
console.error('⚠️ Startup sync error:', syncError);
82101
}
83-
}
84-
85-
await fetchPrompts();
86-
await fetchFolders();
87-
console.log('✅ App initialized');
88-
} catch (error) {
89-
console.error('❌ Init failed:', error);
90-
} finally {
91-
setIsLoading(false);
102+
}, delay);
92103
}
93104
};
94105
init();
106+
107+
// 定时自动同步
108+
const settings = useSettingsStore.getState();
109+
let intervalId: NodeJS.Timeout | null = null;
110+
if (settings.webdavEnabled && settings.webdavAutoSyncInterval > 0 &&
111+
settings.webdavUrl && settings.webdavUsername && settings.webdavPassword) {
112+
const intervalMs = settings.webdavAutoSyncInterval * 60 * 1000;
113+
console.log(`🔄 Auto sync interval: ${settings.webdavAutoSyncInterval} minutes`);
114+
intervalId = setInterval(async () => {
115+
try {
116+
const result = await autoSync({
117+
url: settings.webdavUrl,
118+
username: settings.webdavUsername,
119+
password: settings.webdavPassword,
120+
});
121+
if (result.success) {
122+
console.log('✅ Interval sync completed:', result.message);
123+
await fetchPrompts();
124+
await fetchFolders();
125+
}
126+
} catch (e) {
127+
console.error('⚠️ Interval sync error:', e);
128+
}
129+
}, intervalMs);
130+
}
131+
132+
return () => {
133+
if (intervalId) clearInterval(intervalId);
134+
};
95135
}, []);
96136

97137
if (isLoading) {

src/renderer/components/folder/FolderModal.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,10 @@ export function FolderModal({ isOpen, onClose, folder }: FolderModalProps) {
272272

273273
{isPrivate && (
274274
<div className="pl-6 animate-in fade-in slide-in-from-top-2 duration-200">
275-
{!securityStatus.configured && (
276-
<p className="text-xs text-destructive">请到“设置 - 安全”设置主密码后再开启私密。</p>
277-
)}
278-
{securityStatus.configured && !securityStatus.unlocked && (
279-
<p className="text-xs text-muted-foreground">当前未解锁主密码,私密内容将保持隐藏。</p>
280-
)}
281-
{securityStatus.configured && securityStatus.unlocked && (
282-
<p className="text-xs text-muted-foreground">已解锁主密码,保存后此文件夹内容将加密存储。</p>
275+
{!securityStatus.configured ? (
276+
<p className="text-xs text-destructive">请到"设置 - 安全"设置主密码后再开启私密。</p>
277+
) : (
278+
<p className="text-xs text-muted-foreground">保存后此文件夹内容将加密存储,进入时需要验证密码。</p>
283279
)}
284280
</div>
285281
)}

src/renderer/components/layout/MainContent.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ export function MainContent() {
261261

262262
// AI 测试函数(支持变量替换后的 prompt)
263263
const runAiTest = async (systemPrompt: string | undefined, userPrompt: string, promptId?: string) => {
264-
setShowAiPanel(true);
264+
// 卡片视图不使用弹窗,直接在页面内显示结果
265+
// setShowAiPanel(true); // 移除:不再打开 AiTestModal
265266
setIsTestingAI(true);
266267
setAiResponse(null);
267268
setIsAiTestVariableModalOpen(false);
@@ -960,6 +961,41 @@ export function MainContent() {
960961
)}
961962
</div>
962963
)}
964+
965+
{/* AI 测试响应区域 */}
966+
{(isTestingAI || aiResponse) && (
967+
<div className="mb-4 p-4 rounded-xl bg-card border border-border">
968+
<div className="flex items-center justify-between mb-3">
969+
<div className="flex items-center gap-2">
970+
<SparklesIcon className="w-4 h-4 text-primary" />
971+
<span className="text-sm font-medium">{t('prompt.aiResponse', 'AI 响应')}</span>
972+
<span className="text-xs text-muted-foreground">({aiModel})</span>
973+
</div>
974+
{aiResponse && (
975+
<button
976+
onClick={async () => {
977+
await navigator.clipboard.writeText(aiResponse);
978+
showToast(t('toast.copied'), 'success');
979+
}}
980+
className="p-1.5 rounded hover:bg-muted transition-colors"
981+
title={t('prompt.copy')}
982+
>
983+
<CopyIcon className="w-4 h-4 text-muted-foreground" />
984+
</button>
985+
)}
986+
</div>
987+
{isTestingAI ? (
988+
<div className="flex items-center gap-2 text-muted-foreground">
989+
<LoaderIcon className="w-4 h-4 animate-spin" />
990+
<span className="text-sm">{t('prompt.testing', '测试中...')}</span>
991+
</div>
992+
) : (
993+
<div className="text-sm leading-relaxed whitespace-pre-wrap max-h-60 overflow-y-auto">
994+
{aiResponse}
995+
</div>
996+
)}
997+
</div>
998+
)}
963999
</div>
9641000
</div>
9651001
{/* 操作按钮 - 固定底部 */}
@@ -1051,14 +1087,7 @@ export function MainContent() {
10511087
/>
10521088
)}
10531089

1054-
{/* AI 测试弹窗(变量输入) */}
1055-
<AiTestModal
1056-
isOpen={showAiPanel}
1057-
onClose={() => setShowAiPanel(false)}
1058-
prompt={selectedPrompt || null}
1059-
onUsageIncrement={handleAiUsageIncrement}
1060-
onSaveResponse={handleSaveAiResponse}
1061-
/>
1090+
{/* 卡片视图不使用 AiTestModal,AI 响应直接在页面内显示 */}
10621091

10631092
{/* 变量输入弹窗(用于复制) */}
10641093
{

src/renderer/components/layout/Sidebar.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export function Sidebar({ currentPage, onNavigate }: SidebarProps) {
161161
const [editingFolder, setEditingFolder] = useState<Folder | null>(null);
162162
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
163163
const [passwordFolder, setPasswordFolder] = useState<Folder | null>(null);
164+
const [showAllTags, setShowAllTags] = useState(false);
164165
const filterTags = usePromptStore((state) => state.filterTags);
165166
const toggleFilterTag = usePromptStore((state) => state.toggleFilterTag);
166167

@@ -293,13 +294,21 @@ export function Sidebar({ currentPage, onNavigate }: SidebarProps) {
293294
{/* 标签区域 */}
294295
{uniqueTags.length > 0 && (
295296
<div className="pt-4">
296-
<div className="flex items-center px-3 mb-2">
297+
<div className="flex items-center justify-between px-3 mb-2">
297298
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wider">
298299
{t('nav.tags')}
299300
</span>
301+
{uniqueTags.length > 8 && (
302+
<button
303+
onClick={() => setShowAllTags(!showAllTags)}
304+
className="text-xs text-primary hover:underline"
305+
>
306+
{showAllTags ? t('common.collapse', '收起') : t('common.showAll', `全部 ${uniqueTags.length}`)}
307+
</button>
308+
)}
300309
</div>
301310
<div className="flex flex-wrap gap-1.5 px-3">
302-
{uniqueTags.slice(0, 8).map((tag) => (
311+
{(showAllTags ? uniqueTags : uniqueTags.slice(0, 8)).map((tag) => (
303312
<button
304313
key={tag}
305314
onClick={() => {

src/renderer/components/prompt/CreatePromptModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export function CreatePromptModal({ isOpen, onClose, onCreate }: CreatePromptMod
221221
{/* 图片上传 */}
222222
<div className="space-y-2">
223223
<label className="block text-sm font-medium text-foreground">
224-
{t('prompt.imagesOptional', '参考图片(可选)')}
224+
{t('prompt.referenceImages')}
225225
</label>
226226
<div className="flex flex-wrap gap-3">
227227
{images.map((img, index) => (

src/renderer/components/prompt/EditPromptModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export function EditPromptModal({ isOpen, onClose, prompt }: EditPromptModalProp
231231

232232
{/* 图片管理 */}
233233
<div className="space-y-2">
234-
<label className="block text-sm font-medium text-foreground">{t('prompt.images', '参考图片')}</label>
234+
<label className="block text-sm font-medium text-foreground">{t('prompt.referenceImages')}</label>
235235
<div className="flex flex-wrap gap-3">
236236
{images.map((img, index) => (
237237
<div key={index} className="relative group w-24 h-24 rounded-lg overflow-hidden border border-border">

src/renderer/components/prompt/PromptDetailModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function PromptDetailModal({
197197
{/* 图片 */}
198198
{prompt.images && prompt.images.length > 0 && (
199199
<div>
200-
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('prompt.images', '参考图片')}</h4>
200+
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('prompt.referenceImages')}</h4>
201201
<div className="flex flex-wrap gap-4">
202202
{prompt.images.map((img, index) => (
203203
<div key={index} className="rounded-lg overflow-hidden border border-border shadow-sm">

src/renderer/components/prompt/PromptEditor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect, useCallback, useMemo } from 'react';
2+
import { useTranslation } from 'react-i18next';
23
import { Textarea, Input, Button } from '../ui';
34
import { SaveIcon, XIcon, HashIcon, PlayIcon, CopyIcon, ImageIcon } from 'lucide-react';
45
import type { Prompt } from '../../../shared/types';
@@ -16,6 +17,7 @@ interface PromptEditorProps {
1617
}
1718

1819
export function PromptEditor({ prompt, onSave, onCancel }: PromptEditorProps) {
20+
const { t } = useTranslation();
1921
const [title, setTitle] = useState(prompt.title);
2022
const [description, setDescription] = useState(prompt.description || '');
2123
const [systemPrompt, setSystemPrompt] = useState(prompt.systemPrompt || '');
@@ -184,7 +186,7 @@ export function PromptEditor({ prompt, onSave, onCancel }: PromptEditorProps) {
184186

185187
{/* 图片管理 */}
186188
<div className="space-y-2">
187-
<label className="block text-sm font-medium text-foreground">参考图片</label>
189+
<label className="block text-sm font-medium text-foreground">{t('prompt.referenceImages')}</label>
188190
<div className="flex flex-wrap gap-3">
189191
{images.map((img, index) => (
190192
<div key={index} className="relative group w-24 h-24 rounded-lg overflow-hidden border border-border">

0 commit comments

Comments
 (0)