You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

211 lines
5.2 KiB

<template>
<div class="app-wrapper">
<!-- Topbar -->
<AppTopbar
:search-query="ui.searchQuery"
:user="user"
@update:search-query="ui.searchQuery = $event"
@search="doSearch"
@add="ui.showAddModal = true"
/>
<!-- Body -->
<div class="app-body">
<!-- Sidebar -->
<AppSidebar
:current-view="ui.currentView"
:current-category-id="ui.currentCategoryId"
:current-tag-id="ui.currentTagId"
:categories="categories.tree"
:top-tags="categories.topTags"
@navigate="handleNavigate"
/>
<!-- Page Content -->
<div class="main">
<NuxtPage />
</div>
<!-- Detail Panel -->
<div style="position:relative">
<ItemDetail
:item="detailItem"
:related-items="relatedItems"
:summary-loading="summaryLoading"
@close="ui.closeDetail()"
@select="ui.openDetail($event)"
@delete="items.deleteItem($event)"
@copy-link="copyItemLink($event)"
@write-article="writeArticle($event)"
@generate-summary="generateSummary($event)"
@save-note="saveNote"
@add-tag="addTag"
/>
</div>
</div>
<!-- Add Modal -->
<ItemAddModal
:open="ui.showAddModal"
@close="ui.showAddModal = false"
@save="handleCreate"
/>
<!-- Toast -->
<Toast :message="ui.toastMessage" />
</div>
</template>
<script setup lang="ts">
const { user } = useAuth();
const ui = useUIStore();
const items = useItemsStore();
const categories = useCategoriesStore();
const { search: doSearchFn } = useSearch();
const aiQuery = ref('');
const aiAnswer = ref('');
const aiLoading = ref(false);
const aiResults = ref<any[]>([]);
const summaryLoading = ref(false);
const detailItem = computed(() => {
if (!ui.detailItemId) return null;
return items.list.find(i => i.id === ui.detailItemId) || null;
});
const relatedItems = computed(() => {
if (!detailItem.value) return [];
const tags = detailItem.value.tags?.map((t: any) => t.name) || [];
return items.list
.filter(i => i.id !== detailItem.value.id && i.tags?.some((t: any) => tags.includes(t.name)))
.slice(0, 2);
});
function handleNavigate(type: string, payload?: any) {
if (type === 'articles') {
navigateTo('/articles');
return;
}
if (type === 'settings') {
navigateTo('/settings');
return;
}
// Navigate to /inbox for collection views
ui.navigate(type, payload);
items.page = 1;
items.list = [];
if (useRoute().path !== '/inbox') {
navigateTo('/inbox');
} else {
items.fetchItems();
}
}
function handleCreate(data: any) {
items.createItem(data);
ui.showAddModal = false;
}
function doSearch() {
if (ui.searchQuery.trim()) {
doSearchFn(ui.searchQuery);
items.fetchItems();
}
}
function copyItemLink(item: any) {
if (item.url) {
navigator.clipboard?.writeText(item.url);
}
ui.showToast('链接已复制');
}
async function writeArticle(itemId: number) {
const item = items.list.find(i => i.id === itemId);
if (!item) return;
const content = [
`> 来源: [${item.title}](${item.url || '#'})`,
item.aiSummary ? `> ${item.aiSummary}` : '',
'',
'## 我的想法',
'',
].filter(Boolean).join('\n');
const res = await $fetch<any>('/api/articles', {
method: 'POST',
body: { title: `关于「${item.title}」的笔记`, content },
});
if (res.code === 0) {
await navigateTo(`/articles/${res.data.id}`);
}
}
async function generateSummary(itemId: number) {
summaryLoading.value = true;
try {
const res = await $fetch<any>('/api/ai/summarize', { method: 'POST', body: { itemId } });
if (res.code === 0) {
const item = items.list.find(i => i.id === itemId);
if (item) item.aiSummary = res.data.summary;
ui.showToast('AI 摘要已生成');
}
} finally {
summaryLoading.value = false;
}
}
async function saveNote(itemId: number, note: string) {
const item = items.list.find(i => i.id === itemId);
if (!item) return;
await $fetch(`/api/items/${itemId}`, { method: 'PATCH', body: { note } });
item.note = note;
ui.showToast('备注已保存');
}
async function addTag(itemId: number, tagName: string) {
const item = items.list.find(i => i.id === itemId);
if (!item) return;
const res = await $fetch<any>(`/api/items/${itemId}/tags`, { method: 'POST', body: { tagName } });
if (res.code === 0) {
const newTag = { name: res.data.tagName, id: res.data.tagId };
item.tags = [...(item.tags || []), newTag];
ui.showToast('标签已添加');
}
}
// Init
onMounted(async () => {
await Promise.all([items.fetchItems(), categories.fetchCategories(), categories.fetchTags()]);
});
watch(() => ui.searchQuery, () => {
items.page = 1;
items.list = [];
items.fetchItems();
});
</script>
<style scoped>
.app-wrapper {
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
font-size: 14px;
}
.app-body {
flex: 1;
display: flex;
overflow: hidden;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
position: relative;
}
</style>