Skip to content

Commit 72e686a

Browse files
author
wenhuxian
committed
fix: 修复图片重复请求和加载失败问题,优化UI细节
- 修复 AuthenticatedImage 组件的重复请求问题,添加 pendingRequests 追踪 - 修复 RedmineService 中 API key 重复添加的问题 - 优化分栏间隙,消除视觉间隙 - 添加 Shift+滚轮横向滚动支持 - 隐藏横向滚动条 - 优化附件过滤逻辑,检查 description 和 notes 中的图片引用
1 parent dcc2997 commit 72e686a

File tree

5 files changed

+153
-91
lines changed

5 files changed

+153
-91
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "redmine-desktop",
33
"productName": "Redmine Desktop",
4-
"version": "1.0.20",
4+
"version": "1.0.21",
55
"author": "wenhuxian",
66
"license": "MIT",
77
"description": "Cross-platform Redmine client",

src/renderer/App.tsx

Lines changed: 83 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ const NoteEditor: React.FC<{ issueId: number, onAddNote: (id: number, text: stri
186186
};
187187

188188
return (
189-
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '20px 30px', background: 'var(--editor-bg)', backdropFilter: 'blur(20px)', borderTop: '1px solid var(--editor-border)', zIndex: 10 }}>
190-
<div style={{ position: 'relative' }}>
189+
<div className="note-editor-bar pane-footer">
190+
<div style={{ position: 'relative', flex: 1 }}>
191191
<textarea
192192
className="note-input"
193193
value={noteText}
@@ -199,9 +199,9 @@ const NoteEditor: React.FC<{ issueId: number, onAddNote: (id: number, text: stri
199199
}
200200
}}
201201
placeholder="Add a note... (Shift + Enter to send)"
202-
style={{ width: '100%', height: 44, background: 'var(--input-bg)', border: '1px solid var(--input-border)', borderRadius: 12, padding: '12px 50px 12px 15px', color: 'var(--text-primary)', resize: 'none', fontSize: 13, transition: 'all 0.2s' }}
202+
style={{ width: '100%', height: 36, display: 'block', background: 'rgba(255,255,255,0.05)', border: 'none', borderRadius: 8, padding: '9px 50px 9px 15px', color: 'var(--text-primary)', resize: 'none', fontSize: 13, transition: 'all 0.2s', outline: 'none' }}
203203
onFocus={e => (e.target as any).style.height = '100px'}
204-
onBlur={e => { if (!noteText) (e.target as any).style.height = '44px' }}
204+
onBlur={e => { if (!noteText) (e.target as any).style.height = '36px' }}
205205
/>
206206
<button
207207
onClick={handleSend}
@@ -1470,22 +1470,22 @@ const App: React.FC = () => {
14701470
</div>
14711471

14721472
{/* Sidebar Footer with Settings */}
1473-
<div style={{ padding: '15px', borderTop: '1px solid #222', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
1473+
<div className="pane-footer" style={{ borderTop: '1px solid var(--border-color)' }}>
14741474
<div
14751475
className="sidebar-item"
14761476
onClick={() => setShowSettings(true)}
1477-
style={{ margin: 0, padding: '8px 12px', flex: 1, textAlign: 'center', background: 'rgba(255,255,255,0.05)', borderRadius: 8, fontSize: 13 }}
1477+
style={{ margin: 0, padding: 0, flex: 1, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(255,255,255,0.05)', borderRadius: 8, fontSize: 13 }}
14781478
>
14791479
⚙️ Settings
14801480
</div>
14811481
</div>
14821482
</aside>
14831483

1484-
{/* Sidebar Resize Handle */}
14851484
<div
1485+
className="no-drag"
14861486
onMouseDown={() => setResizingPane('sidebar')}
1487-
style={{ width: 4, cursor: 'col-resize', background: resizingPane === 'sidebar' ? '#0c66ff' : 'transparent', flexShrink: 0 }}
1488-
onMouseEnter={e => (e.target as HTMLDivElement).style.background = '#333'}
1487+
style={{ width: 4, cursor: 'col-resize', background: resizingPane === 'sidebar' ? '#0c66ff' : 'transparent', flexShrink: 0, margin: '0 -2px', zIndex: 100, transition: 'background 0.2s' }}
1488+
onMouseEnter={e => (e.target as HTMLDivElement).style.background = '#0c66ff'}
14891489
onMouseLeave={e => (e.target as HTMLDivElement).style.background = resizingPane === 'sidebar' ? '#0c66ff' : 'transparent'}
14901490
/>
14911491

@@ -1705,7 +1705,7 @@ const App: React.FC = () => {
17051705

17061706
<div
17071707
ref={issueListRef}
1708-
style={{ overflowY: 'auto', flex: 1, paddingBottom: 60, position: 'relative' }}
1708+
style={{ overflowY: 'auto', flex: 1, paddingBottom: 20, position: 'relative' }}
17091709
>
17101710
<div className="issue-list-content" ref={listRef}>
17111711
{/* Sliding Selection Indicator */}
@@ -1778,64 +1778,66 @@ const App: React.FC = () => {
17781778
</div>
17791779
</div>
17801780

1781-
<div className="add-task-bar" style={{ position: 'absolute', bottom: 0, left: 0, right: 0 }}>
1782-
<span style={{ color: '#0c66ff', fontSize: 20, cursor: 'pointer' }}>+</span>
1783-
<input type="text" placeholder="快速添加任务..." value={newTaskSubject} onChange={e => setNewTaskSubject(e.target.value)} onKeyDown={e => {
1784-
if (e.key === 'Enter' && newTaskSubject.trim() && vm.selectedProjectId !== -1) {
1785-
vm.createIssue(newTaskSubject, vm.selectedProjectId!, quickAddVersionId || undefined, quickAddAssigneeId || undefined);
1786-
setNewTaskSubject('');
1787-
}
1788-
}} style={{ flex: 1, background: 'transparent', border: 'none', color: 'var(--text-primary)', padding: 8 }} />
1789-
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
1790-
{vm.selectedProjectId !== -1 && vm.selectedProjectId !== null && (
1781+
<div className="add-task-bar pane-footer">
1782+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, background: 'rgba(255,255,255,0.05)', borderRadius: 8, padding: '0 12px', flex: 1, height: 36 }}>
1783+
<span style={{ color: '#0c66ff', fontSize: 18, cursor: 'pointer', display: 'flex', alignItems: 'center' }}>+</span>
1784+
<input type="text" placeholder="快速添加任务..." value={newTaskSubject} onChange={e => setNewTaskSubject(e.target.value)} onKeyDown={e => {
1785+
if (e.key === 'Enter' && newTaskSubject.trim() && vm.selectedProjectId !== -1) {
1786+
vm.createIssue(newTaskSubject, vm.selectedProjectId!, quickAddVersionId || undefined, quickAddAssigneeId || undefined);
1787+
setNewTaskSubject('');
1788+
}
1789+
}} style={{ flex: 1, background: 'transparent', border: 'none', color: 'var(--text-primary)', padding: '4px 0', fontSize: 13, outline: 'none' }} />
1790+
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
1791+
{vm.selectedProjectId !== -1 && vm.selectedProjectId !== null && (
1792+
<span style={{ fontSize: 11, color: '#888', position: 'relative' }}>
1793+
{(vm.projectVersionsMap[vm.selectedProjectId] || []).find((v: { id: number }) => v.id === quickAddVersionId)?.name || '无版本'}
1794+
<select
1795+
value={quickAddVersionId || ''}
1796+
onChange={e => setQuickAddVersionId(e.target.value ? parseInt(e.target.value) : null)}
1797+
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
1798+
>
1799+
<option value="">无版本</option>
1800+
{(vm.projectVersionsMap[vm.selectedProjectId] || []).map((v: { id: number; name: string }) => (
1801+
<option key={v.id} value={v.id}>{v.name}</option>
1802+
))}
1803+
</select>
1804+
<span style={{ marginLeft: 3, fontSize: 10, color: '#666' }}></span>
1805+
</span>
1806+
)}
17911807
<span style={{ fontSize: 11, color: '#888', position: 'relative' }}>
1792-
{(vm.projectVersionsMap[vm.selectedProjectId] || []).find((v: { id: number }) => v.id === quickAddVersionId)?.name || '无版本'}
1808+
{quickAddAssigneeId === null
1809+
? '👤 暂未指派'
1810+
: currentProjectMembers.find(m => m.id === quickAddAssigneeId)?.name || '👤 暂未指派'}
17931811
<select
1794-
value={quickAddVersionId || ''}
1795-
onChange={e => setQuickAddVersionId(e.target.value ? parseInt(e.target.value) : null)}
1812+
value={quickAddAssigneeId || ''}
1813+
onChange={e => setQuickAddAssigneeId(e.target.value ? parseInt(e.target.value) : null)}
17961814
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
17971815
>
1798-
<option value="">无版本</option>
1799-
{(vm.projectVersionsMap[vm.selectedProjectId] || []).map((v: { id: number; name: string }) => (
1800-
<option key={v.id} value={v.id}>{v.name}</option>
1801-
))}
1816+
<option value="">👤 暂未指派</option>
1817+
{renderGroupedMemberOptions(currentProjectMembers)}
18021818
</select>
18031819
<span style={{ marginLeft: 3, fontSize: 10, color: '#666' }}></span>
18041820
</span>
1821+
</div>
1822+
{vm.selectedProjectId === -1 && (
1823+
<div style={{ fontSize: 11, color: '#444' }}>请选择项目</div>
18051824
)}
1806-
<span style={{ fontSize: 11, color: '#888', position: 'relative' }}>
1807-
{quickAddAssigneeId === null
1808-
? '👤 暂未指派'
1809-
: currentProjectMembers.find(m => m.id === quickAddAssigneeId)?.name || '👤 暂未指派'}
1810-
<select
1811-
value={quickAddAssigneeId || ''}
1812-
onChange={e => setQuickAddAssigneeId(e.target.value ? parseInt(e.target.value) : null)}
1813-
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
1814-
>
1815-
<option value="">👤 暂未指派</option>
1816-
{renderGroupedMemberOptions(currentProjectMembers)}
1817-
</select>
1818-
<span style={{ marginLeft: 3, fontSize: 10, color: '#666' }}></span>
1819-
</span>
18201825
</div>
1821-
{vm.selectedProjectId === -1 && (
1822-
<div style={{ fontSize: 11, color: '#444' }}>请选择项目</div>
1823-
)}
18241826
</div>
18251827
</section>
18261828

1827-
{/* List Resize Handle */}
18281829
<div
1830+
className="no-drag"
18291831
onMouseDown={() => setResizingPane('list')}
1830-
style={{ width: 4, cursor: 'col-resize', background: resizingPane === 'list' ? '#0c66ff' : 'transparent', flexShrink: 0 }}
1831-
onMouseEnter={e => (e.target as HTMLDivElement).style.background = '#333'}
1832+
style={{ width: 4, cursor: 'col-resize', background: resizingPane === 'list' ? '#0c66ff' : 'transparent', flexShrink: 0, margin: '0 -2px', zIndex: 100, transition: 'background 0.2s' }}
1833+
onMouseEnter={e => (e.target as HTMLDivElement).style.background = '#0c66ff'}
18321834
onMouseLeave={e => (e.target as HTMLDivElement).style.background = resizingPane === 'list' ? '#0c66ff' : 'transparent'}
18331835
/>
18341836

18351837
{/* Detail */}
18361838
<main className="issue-detail-pane" ref={detailPaneRef}>
18371839
{selectedIssue ? (
1838-
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative' }}>
1840+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'stretch' }}>
18391841
<div style={{ padding: '40px 30px 20px' }}>
18401842
{/* ID and actions row */}
18411843
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
@@ -2032,7 +2034,15 @@ const App: React.FC = () => {
20322034
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 15 }}>创建人:{selectedIssue.author.name} • 时间:{format(new Date(selectedIssue.created_on), 'yyyy-MM-dd HH:mm')}</div>
20332035
</div>
20342036

2035-
<div style={{ flex: 1, overflowY: 'auto', paddingBottom: 100 }}>
2037+
<div
2038+
style={{ flex: 1, overflowY: 'auto', overflowX: 'auto', paddingBottom: 20 }}
2039+
onWheel={(e) => {
2040+
if (e.shiftKey && e.deltaY !== 0) {
2041+
e.currentTarget.scrollLeft += e.deltaY;
2042+
e.preventDefault();
2043+
}
2044+
}}
2045+
>
20362046
<div style={{ padding: '0 30px' }}>
20372047
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
20382048
<h3 style={{ fontSize: 13, fontWeight: 500, color: '#666', textTransform: 'uppercase' }}>Description</h3>
@@ -2111,28 +2121,37 @@ const App: React.FC = () => {
21112121
{/* Attachments Section */}
21122122
{(() => {
21132123
const currentDescription = editingDescription ? editDescriptionValue : (selectedIssue.description || '');
2124+
const journals = selectedIssue.journals || [];
2125+
const allNotes = journals.map((j: IssueJournal) => j.notes || '').join('\n');
2126+
const allText = currentDescription + '\n' + allNotes;
21142127

2115-
const isImageReferenced = (filename: string, text: string) => {
2128+
const isImageReferenced = (filename: string, contentUrl: string | undefined, text: string) => {
21162129
if (!text) return false;
2130+
2131+
// Check if full content_url appears in text (most reliable)
2132+
if (contentUrl && text.includes(contentUrl)) return true;
2133+
21172134
const encoded = encodeURIComponent(filename);
2135+
const escapedFilename = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2136+
const escapedEncoded = encoded.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21182137

2119-
// Check for Textile image syntax: !filename! or !filename(alt)!
2120-
const textilePattern = new RegExp(`!${filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\([^)]*\\))?!`, 'g');
2121-
const textileEncodedPattern = new RegExp(`!${encoded.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\([^)]*\\))?!`, 'g');
2138+
// Check for Textile image syntax: !filename!
2139+
const textilePattern = new RegExp(`!${escapedFilename}!`, 'i');
2140+
const textileEncodedPattern = new RegExp(`!${escapedEncoded}!`, 'i');
21222141

2123-
// Check for Markdown image syntax: ![alt](attachment:filename) or ![alt](filename)
2124-
const markdownPattern = new RegExp(`!\\[[^\\]]*\\]\\((?:attachment:)?${filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 'g');
2125-
const markdownEncodedPattern = new RegExp(`!\\[[^\\]]*\\]\\((?:attachment:)?${encoded.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 'g');
2142+
// Check for Markdown image syntax: ![](filename) or ![](attachment:filename)
2143+
const hasMarkdown = text.includes(`(${filename})`) ||
2144+
text.includes(`(${encoded})`) ||
2145+
text.includes(`(attachment:${filename})`) ||
2146+
text.includes(`(attachment:${encoded})`);
21262147

2127-
return textilePattern.test(text) ||
2128-
textileEncodedPattern.test(text) ||
2129-
markdownPattern.test(text) ||
2130-
markdownEncodedPattern.test(text);
2148+
if (hasMarkdown) return true;
2149+
return textilePattern.test(text) || textileEncodedPattern.test(text);
21312150
};
21322151

21332152
const filteredAttachments = selectedIssue.attachments?.filter(a => {
21342153
if (a.content_type?.startsWith('image/')) {
2135-
if (isImageReferenced(a.filename, currentDescription)) return false;
2154+
if (isImageReferenced(a.filename, a.content_url, allText)) return false;
21362155
}
21372156
return true;
21382157
}) || [];
@@ -2141,12 +2160,12 @@ const App: React.FC = () => {
21412160
if (u.content_type?.startsWith('image/')) {
21422161
// Check if tempUrl is used in markdown image syntax: ![](data:...) or textile !data:...!
21432162
const isTempInImage = u.tempUrl && (
2144-
currentDescription.includes(`](${u.tempUrl})`) ||
2145-
currentDescription.includes(`!${u.tempUrl}!`)
2163+
allText.includes(`](${u.tempUrl})`) ||
2164+
allText.includes(`!${u.tempUrl}!`)
21462165
);
21472166

21482167
if (isTempInImage) return false;
2149-
if (isImageReferenced(u.filename, currentDescription)) return false;
2168+
if (isImageReferenced(u.filename, undefined, allText)) return false;
21502169
}
21512170
return true;
21522171
});

0 commit comments

Comments
 (0)