Skip to content

Commit 2d43e7f

Browse files
feat: 支持通过URL打开项目并改进附件排序
- 实现通过URL hash (#/project/{projectId}) 打开指定项目 - 支持项目不存在时的错误提示和URL重置 - 附件画廊按ID降序排序,较新的附件排在前面 - 优化代码质量:关闭编辑器occurrencesHighlight - 更新国际化文本:添加项目不存在错误提示
1 parent 90c1b9b commit 2d43e7f

11 files changed

Lines changed: 121 additions & 22 deletions

File tree

src/components/AppInitializer.tsx

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,37 @@
33
* 负责初始化示例数据并自动加载到UI
44
*/
55

6-
import { useEffect, useState, useRef } from 'react';
6+
import { useEffect, useState, useRef, useCallback } from 'react';
77
import { initializeSampleData } from '@/services/initializeSampleData';
88
import { useProjectStore } from '@/store/projectStore';
99
import { useVersionStore } from '@/store/versionStore';
1010
import { useSettingsStore } from '@/store/settingsStore';
11+
import { useTranslation } from '@/i18n/I18nContext';
1112
import { storage, STORAGE_KEYS } from '@/utils/storage';
13+
import { db } from '@/db/schema';
1214

1315
interface AppInitializerProps {
1416
children: React.ReactNode;
1517
}
1618

19+
/**
20+
* 从 URL hash 中解析项目 ID
21+
* 格式: #/project/{projectId}
22+
*/
23+
const getProjectIdFromUrl = (): string | null => {
24+
const hash = window.location.hash;
25+
if (!hash) return null;
26+
27+
const match = hash.match(/^#\/project\/(.+)$/);
28+
return match ? match[1] : null;
29+
};
30+
1731
export const AppInitializer: React.FC<AppInitializerProps> = ({ children }) => {
1832
const [isInitialized, setIsInitialized] = useState(false);
19-
const { loadProjects, setCurrentProject, loadFolders } = useProjectStore();
33+
const { loadProjects, setCurrentProject, loadFolders, expandFolderPathToProject } = useProjectStore();
2034
const { loadVersions } = useVersionStore();
2135
const { theme } = useSettingsStore();
36+
const t = useTranslation();
2237

2338
// 使用 ref 确保初始化只执行一次(防止 React 18 严格模式下的双重调用)
2439
const hasInitialized = useRef(false);
@@ -37,6 +52,13 @@ export const AppInitializer: React.FC<AppInitializerProps> = ({ children }) => {
3752
applyTheme(theme === 'dark');
3853
}, [theme]);
3954

55+
// 处理从 URL 打开项目的函数
56+
const handleOpenProjectFromUrl = useCallback(async (projectId: string) => {
57+
setCurrentProject(projectId);
58+
await expandFolderPathToProject(projectId);
59+
await loadVersions(projectId);
60+
}, [setCurrentProject, expandFolderPathToProject, loadVersions]);
61+
4062
useEffect(() => {
4163
// 如果已经初始化过,直接返回
4264
if (hasInitialized.current) {
@@ -48,10 +70,26 @@ export const AppInitializer: React.FC<AppInitializerProps> = ({ children }) => {
4870
// 初始化示例数据
4971
const sampleProjectId = await initializeSampleData();
5072

51-
// 如果创建了示例项目,自动加载和选择它
52-
if (sampleProjectId) {
53-
await loadFolders();
54-
await loadProjects();
73+
// 加载文件夹和项目
74+
await loadFolders();
75+
await loadProjects();
76+
77+
// 检查 URL 中是否有项目 ID 参数
78+
const urlProjectId = getProjectIdFromUrl();
79+
80+
if (urlProjectId) {
81+
// 验证项目是否存在
82+
const project = await db.projects.get(urlProjectId);
83+
if (project) {
84+
// 项目存在,打开它
85+
await handleOpenProjectFromUrl(urlProjectId);
86+
} else {
87+
// 项目不存在,提示用户并重置 URL
88+
alert(t('errors.projectNotFound'));
89+
window.open('/', '_self');
90+
}
91+
} else if (sampleProjectId) {
92+
// 如果创建了示例项目,自动加载和选择它
5593
setCurrentProject(sampleProjectId);
5694
await loadVersions(sampleProjectId);
5795
}
@@ -65,7 +103,7 @@ export const AppInitializer: React.FC<AppInitializerProps> = ({ children }) => {
65103
};
66104

67105
initialize();
68-
}, [loadProjects, loadFolders, setCurrentProject, loadVersions]);
106+
}, [loadProjects, loadFolders, setCurrentProject, loadVersions, handleOpenProjectFromUrl, t]);
69107

70108
// 在初始化完成前,可以显示一个简单的加载提示
71109
if (!isInitialized) {

src/components/editor/PromptEditor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ const PromptEditor = forwardRef<PromptEditorRef, PromptEditorProps>(
231231
},
232232
overviewRulerLanes: 0,
233233
overviewRulerBorder: false,
234+
occurrencesHighlight: 'off',
234235
}}
235236
/>
236237
{isTouchDevice && (

src/components/layout/FolderTree.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,20 @@ const ProjectItemConnected: React.FC<{
267267
onCloseAllMenus?: () => void;
268268
onContextMenu: (e: React.MouseEvent, item: Folder | Project) => void;
269269
}> = ({ project, level, onCloseAllMenus, onContextMenu }) => {
270-
const { currentProjectId, selectProject } = useProjectStore();
270+
const { currentProjectId, selectProject, expandFolderPathToProject } = useProjectStore();
271+
272+
const handleSelect = async (projectId: string) => {
273+
selectProject(projectId, { updateUrl: true });
274+
await expandFolderPathToProject(projectId);
275+
};
271276

272277
return (
273278
<ProjectItem
274279
project={project}
275280
level={level}
276281
onContextMenu={(e, p) => onContextMenu(e, p)}
277282
isSelected={currentProjectId === project.id}
278-
onSelect={selectProject}
283+
onSelect={handleSelect}
279284
onCloseAllMenus={onCloseAllMenus}
280285
/>
281286
);
@@ -439,7 +444,7 @@ export const FolderTree: React.FC<FolderTreeProps> = ({ onProjectSelect: _onProj
439444
if (projectName && projectName.trim()) {
440445
const projectId = await createProject(projectName.trim(), folderContextMenu.folder.id);
441446
await loadProjects();
442-
selectProject(projectId);
447+
selectProject(projectId, { updateUrl: true });
443448
expandFolder(folderContextMenu.folder.id);
444449
}
445450
}

src/components/layout/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const Sidebar: React.FC = () => {
3232
await loadFolders();
3333
const projectId = await createProject(projectName.trim(), rootFolderId);
3434
await loadProjects();
35-
selectProject(projectId);
35+
selectProject(projectId, { updateUrl: true });
3636
}
3737
};
3838

src/components/version/AttachmentGallery.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ export const AttachmentGallery: React.FC<AttachmentGalleryProps> = ({
3737
const notes = currentVersion?.notes || '';
3838

3939
// 过滤出可预览的图片
40-
const previewableAttachments = attachments.filter(
41-
(att) => !att.isMissing && att.fileType.startsWith('image/')
42-
);
40+
const previewableAttachments = attachments
41+
.filter((att) => !att.isMissing && att.fileType.startsWith('image/'))
42+
// 按 ID 降序排序,较新的附件排在前面
43+
.sort((a, b) => b.id.localeCompare(a.id));
4344

4445
// 当前正在预览的附件对象
4546
const currentPreviewAttachment =
@@ -180,9 +181,11 @@ export const AttachmentGallery: React.FC<AttachmentGalleryProps> = ({
180181
{/* Version Meta Card - now integrated */}
181182
<VersionMetaCard versionId={versionId} score={score} notes={notes} readonly={readonly} />
182183

183-
{/* Attachment Items */}
184+
{/* Attachment Items - 按 ID 降序排序,较新的附件排在前面 */}
184185
<AnimatePresence>
185-
{attachments.map((attachment) => (
186+
{attachments
187+
.sort((a, b) => b.id.localeCompare(a.id))
188+
.map((attachment) => (
186189
<motion.div
187190
key={attachment.id}
188191
initial={{ opacity: 0, scale: 0.9 }}
@@ -247,7 +250,7 @@ export const AttachmentGallery: React.FC<AttachmentGalleryProps> = ({
247250
e.stopPropagation();
248251
handlePreview(attachment);
249252
}}
250-
className="p-1.5 bg-surface-containerHighest text-surface-onSurface rounded-md shadow-sm "
253+
className="p-1.5 !backdrop-blur-3xl !bg-surface-containerHighest/60 text-surface-onSurface rounded-md shadow-sm "
251254
title={t('components.attachmentGallery.preview')}
252255
>
253256
<Icons.Eye size={12} />
@@ -259,7 +262,7 @@ export const AttachmentGallery: React.FC<AttachmentGalleryProps> = ({
259262
e.stopPropagation();
260263
handleDownload(attachment);
261264
}}
262-
className="p-1.5 bg-surface-containerHighest text-surface-onSurface rounded-md shadow-sm "
265+
className="p-1.5 !backdrop-blur-3xl !bg-surface-containerHighest/60 text-surface-onSurface rounded-md shadow-sm "
263266
title={t('components.attachmentGallery.download')}
264267
>
265268
<Icons.Download size={12} />
@@ -271,7 +274,7 @@ export const AttachmentGallery: React.FC<AttachmentGalleryProps> = ({
271274
e.stopPropagation();
272275
handleDelete(attachment.id);
273276
}}
274-
className="p-1.5 bg-surface-containerHighest hover:bg-surface-containerHighest"
277+
className="p-1.5 !backdrop-blur-3xl"
275278
title={t('common.delete')}
276279
>
277280
<Icons.Trash size={12} />

src/i18n/locales/en-US.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,6 @@ export const enUS: TranslationData = {
236236
deleteFailed: 'Delete failed',
237237
exportFailed: 'Export failed',
238238
importFailed: 'Import failed',
239+
projectNotFound: 'Project not found or has been deleted',
239240
},
240241
};

src/i18n/locales/zh-CN.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,6 @@ export const zhCN: TranslationData = {
231231
deleteFailed: '删除失败',
232232
exportFailed: '导出失败',
233233
importFailed: '导入失败',
234+
projectNotFound: '项目不存在或已被删除',
234235
},
235236
};

src/i18n/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,5 +289,6 @@ export interface TranslationData {
289289
deleteFailed: string;
290290
exportFailed: string;
291291
importFailed: string;
292+
projectNotFound: string;
292293
};
293294
}

src/router.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export const router = createHashRouter([
77
path: '/',
88
element: <MainView />,
99
},
10+
{
11+
path: '/project/:projectId',
12+
element: <MainView />,
13+
},
1014
{
1115
path: '/settings',
1216
element: <Settings />,

src/services/attachmentManager.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import type { Attachment } from '@/models/Attachment';
77
import { nanoid } from 'nanoid';
88

99
export class AttachmentManager {
10+
/**
11+
* 生成带时间戳前缀的附件 ID
12+
* 使用秒级时间戳作为前缀,便于排序
13+
*/
14+
private generateAttachmentId(): string {
15+
const timestamp = Math.floor(Date.now() / 1000);
16+
return `${timestamp}_${nanoid()}`;
17+
}
18+
1019
/**
1120
* 上传附件
1221
*/
@@ -15,7 +24,7 @@ export class AttachmentManager {
1524
const blob = new Blob([buffer], { type: file.type });
1625

1726
const attachment: Attachment = {
18-
id: nanoid(),
27+
id: this.generateAttachmentId(),
1928
versionId,
2029
fileName: file.name,
2130
fileType: file.type,

0 commit comments

Comments
 (0)