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
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>
|
|
|