refactor(frontend): extract NotesTab safely

- New component: frontend/src/lib/components/notes/NotesTab.svelte
  - Owns notes list UI, create-note form, note cards with rename/delete
  - Props: notes array, formatDate
  - Events: submitCreateNote, openNote, startRename, deleteNote

- App.svelte changes:
  - Removed inline notes tab markup (replaced with <NotesTab>)
  - Removed showCreateNote, newNoteTitle state variables
  - Removed openCreateNote, cancelCreateNote, submitCreateNote functions
  - Added _handleSubmitCreateNote() event handler from NotesTab
  - Removed notes-related CSS (notes-tab, note-card, note-action-*)
  - Fixed import paths in FilesTab (../../ instead of ../ for lib/ files)

- Build: npm run build ✅, go test ./... ✅, build.sh gui ✅
This commit is contained in:
2026-06-16 03:21:49 +08:00
parent acdbbdfa55
commit 58cdd61d27
8 changed files with 221 additions and 4963 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -19,8 +19,8 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-Da3BSkUM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
<script type="module" crossorigin src="/assets/main-nSPvOkeD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CUROQg6p.css">
</head>
<body>
<div id="app"></div>
+17 -75
View File
@@ -18,6 +18,7 @@
import { t } from './lib/i18n'
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
import NotesTab from './lib/components/notes/NotesTab.svelte'
import FilesTab from './lib/components/files/FilesTab.svelte'
// ===== Wails v2 API call helper =====
@@ -144,8 +145,6 @@
let createInNode = null
let createWithTemplate = null
let contextMenu = { visible: false, x: 0, y: 0, node: null }
let showCreateNote = false
let newNoteTitle = ''
let showCreateAction = false
let newActionTitle = ''
let newActionKind = 'open_url'
@@ -298,7 +297,6 @@
if (showRename) { showRename = false; return true }
if (showWorklogModal) { closeWorklogModal(); return true }
if (showCreateAction) { cancelCreateAction(); return true }
if (showCreateNote) { cancelCreateNote(); return true }
if (showCreateNode) { cancelCreateNode(); return true }
if (contextMenu.visible) { closeContextMenu(); return true }
if (noteEditor) { closeNoteEditor(); return true }
@@ -513,7 +511,6 @@
resetTrashBrowser()
noteEditor = null
showCreateNode = false
showCreateNote = false
error = ''
caseActivity = []
await loadTabData(node.id)
@@ -915,21 +912,17 @@
}
// ===== Notes =====
function openCreateNote() { showCreateNote = true; newNoteTitle = '' }
function cancelCreateNote() { showCreateNote = false; newNoteTitle = '' }
async function submitCreateNote() {
if (!newNoteTitle.trim() || !selectedNode) return
// Notes create/delete/rename are handled by NotesTab component via events.
// App.svelte keeps note editor lifecycle (doOpenNote, saveCurrentNote, etc.)
async function _handleSubmitCreateNote(title) {
if (!title.trim() || !selectedNode) return
try {
const note = await wailsCall('CreateNote', selectedNode.id, newNoteTitle.trim())
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }]
showCreateNote = false
newNoteTitle = ''
const note = await wailsCall('CreateNote', selectedNode.id, title.trim())
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: title.trim(), createdAt: new Date().toISOString() }]
} catch (e) {
// Fallback: create note locally
const newNote = { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }
const newNote = { id: Date.now().toString(), title: title.trim(), createdAt: new Date().toISOString() }
notes = [...notes, newNote]
showCreateNote = false
newNoteTitle = ''
}
}
@@ -2613,43 +2606,14 @@
</div>
{:else if activeTab === 'notes'}
<div class="notes-tab">
<div class="tab-toolbar">
<button class="btn btn-primary" on:click={openCreateNote}>{t('note.add')}</button>
</div>
{#if showCreateNote}
<div class="create-form">
<input type="text" placeholder={t('note.title')} bind:value={newNoteTitle}
on:keydown={(e) => e.key === 'Enter' && submitCreateNote()} />
<div class="form-actions">
<button class="btn btn-primary" on:click={submitCreateNote}>{t('common.create')}</button>
<button class="btn" on:click={cancelCreateNote}>{t('common.cancel')}</button>
</div>
</div>
{/if}
{#if notes.length === 0 && !showCreateNote}
<div class="empty-state"><p>{t('note.noNotes')}</p><p class="hint">{t('note.createFirst')}</p></div>
{:else}
<div class="notes-list">
{#each notes as note}
<div class="note-card" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
<div class="note-card-info">
<div class="note-card-title">{note.title}</div>
<div class="note-card-date">{formatDate(note.createdAt)}</div>
</div>
<div class="note-card-actions" on:click|stopPropagation>
<button class="note-action-btn" on:click={() => startRenameNote(note.id, note.title)} title={t('common.rename')}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
</button>
<button class="note-action-btn note-action-danger" on:click={() => deleteNote(note)} title={t('common.delete')}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<NotesTab
{notes}
{formatDate}
on:submitCreateNote={(e) => _handleSubmitCreateNote(e.detail.title)}
on:openNote={(e) => openNote(e.detail.note)}
on:startRename={(e) => startRenameNote(e.detail.noteId, e.detail.currentTitle)}
on:deleteNote={(e) => deleteNote(e.detail.note)}
/>
{:else if activeTab === 'files'}
<FilesTab
@@ -3714,15 +3678,6 @@
.note-rename-input { flex: 1; padding: 6px 10px; border: 1px solid #2a2a3c; border-radius: 4px; background: #13131f; color: #e4e4ef; font-size: 14px; font-family: inherit; outline: none; }
.note-rename-input:focus { border-color: #818cf8; }
/* Note card actions */
.note-card { position: relative; }
.note-card-info { flex: 1; min-width: 0; }
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
.note-card:hover .note-card-actions { opacity: 1; }
.note-action-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: #666; cursor: pointer; transition: background 0.12s, color 0.12s; }
.note-action-btn:hover { background: #2a2a3c; color: #ccc; }
.note-action-danger:hover { background: rgba(239, 68, 68, 0.15); color: #f87171; }
/* Form groups in modals */
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 12px; color: #888; margin-bottom: 4px; }
@@ -3747,19 +3702,6 @@
.recent-date { font-size: 11px; color: #555; }
.recent-entry { padding: 6px 0; font-size: 13px; color: #888; border-bottom: 1px solid #1a1a28; }
/* Notes tab */
.notes-tab { padding: 24px; }
.tab-toolbar { margin-bottom: 16px; }
.create-form { background: #1a1a28; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.create-form input { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; margin-bottom: 8px; }
.create-form input:focus { outline: none; border-color: #6366f1; }
.form-actions { display: flex; gap: 8px; }
.notes-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.note-card { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 16px; cursor: pointer; }
.note-card:hover { border-color: #3a3a5c; }
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.note-card-date { font-size: 11px; color: #555; }
/* Worklog tab */
.worklog-tab { padding: 24px; }
.worklog-toolbar { margin-bottom: 16px; }
File diff suppressed because it is too large Load Diff
@@ -2,10 +2,10 @@
import { createEventDispatcher } from 'svelte'
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
import FileTreeRow from '../../../FileTreeRow.svelte'
import FilePreviewModal from '../FilePreviewModal.svelte'
import ConfirmModal from '../ConfirmModal.svelte'
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../fileUtils.js'
import { t } from '../i18n'
import FilePreviewModal from '../../FilePreviewModal.svelte'
import ConfirmModal from '../../ConfirmModal.svelte'
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../../fileUtils.js'
import { t } from '../../i18n'
// ===== Props =====
export let selectedNode = null
@@ -0,0 +1,110 @@
<script>
import { createEventDispatcher } from 'svelte'
// ===== Props =====
export let notes = []
export let formatDate = (str) => ''
// ===== Events =====
const dispatch = createEventDispatcher()
// ===== Internal state =====
let showCreateNote = false
let newNoteTitle = ''
function openCreateNote() {
showCreateNote = true
newNoteTitle = ''
}
function cancelCreateNote() {
showCreateNote = false
newNoteTitle = ''
}
function submitCreateNote() {
if (!newNoteTitle.trim()) return
dispatch('submitCreateNote', { title: newNoteTitle.trim() })
showCreateNote = false
newNoteTitle = ''
}
function handleCreateKeydown(e) {
if (e.key === 'Enter') submitCreateNote()
}
function openNote(note) {
dispatch('openNote', { note })
}
function startRename(noteId, currentTitle) {
dispatch('startRename', { noteId, currentTitle })
}
function deleteNoteHandler(note) {
dispatch('deleteNote', { note })
}
</script>
<div class="notes-tab">
<div class="tab-toolbar">
<button class="btn btn-primary" on:click={openCreateNote}>Добавить заметку</button>
</div>
{#if showCreateNote}
<div class="create-form">
<input type="text" placeholder="Название заметки" bind:value={newNoteTitle}
on:keydown={handleCreateKeydown} />
<div class="form-actions">
<button class="btn btn-primary" on:click={submitCreateNote}>Создать</button>
<button class="btn" on:click={cancelCreateNote}>Отмена</button>
</div>
</div>
{/if}
{#if notes.length === 0 && !showCreateNote}
<div class="empty-state"><p>Нет заметок</p><p class="hint">Создайте первую заметку</p></div>
{:else if notes.length > 0}
<div class="notes-list">
{#each notes as note}
<div class="note-card" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={(e) => e.key === 'Enter' && openNote(note)}>
<div class="note-card-info">
<div class="note-card-title">{note.title}</div>
<div class="note-card-date">{formatDate(note.createdAt)}</div>
</div>
<div class="note-card-actions" on:click|stopPropagation>
<button class="note-action-btn" on:click={() => startRename(note.id, note.title)} title="Переименовать">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
</button>
<button class="note-action-btn note-action-danger" on:click={() => deleteNoteHandler(note)} title="Удалить">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.notes-tab { padding: 24px; }
.tab-toolbar { margin-bottom: 16px; }
.create-form { background: #1a1a28; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.create-form input { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; margin-bottom: 8px; }
.create-form input:focus { outline: none; border-color: #6366f1; }
.form-actions { display: flex; gap: 8px; }
.notes-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.note-card { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 16px; cursor: pointer; position: relative; }
.note-card:hover { border-color: #3a3a5c; }
.note-card-info { flex: 1; min-width: 0; }
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.note-card-date { font-size: 11px; color: #555; }
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
.note-card:hover .note-card-actions { opacity: 1; }
.note-action-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: #666; cursor: pointer; transition: background 0.12s, color 0.12s; }
.note-action-btn:hover { background: #2a2a3c; color: #ccc; }
.note-action-danger:hover { background: rgba(239, 68, 68, 0.15); color: #f87171; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
.empty-state p { margin: 0; font-size: 14px; color: #666; }
.empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; }
</style>