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.
283 lines
8.3 KiB
283 lines
8.3 KiB
<script setup lang="ts">
|
|
definePageMeta({ layout: 'default' });
|
|
|
|
const ui = useUIStore();
|
|
const items = useItemsStore();
|
|
const categories = useCategoriesStore();
|
|
const { search: doSearchFn } = useSearch();
|
|
|
|
const scrollArea = ref<HTMLElement>();
|
|
const aiQuery = ref('');
|
|
const aiAnswer = ref('');
|
|
const aiLoading = ref(false);
|
|
const aiResults = ref<any[]>([]);
|
|
|
|
const pageTitle = computed(() => {
|
|
if (ui.currentView === 'inbox') return '收件箱';
|
|
if (ui.currentView === 'starred') return '已加星标';
|
|
if (ui.currentView === 'recent') return '最近收藏';
|
|
if (ui.currentView === 'all') return '全部收藏';
|
|
if (ui.currentView === 'category') return categories.tree.find((c: any) => c.id === ui.currentCategoryId)?.name || '分类';
|
|
if (ui.currentView === 'tag') return '#' + (categories.tags.find((t: any) => t.id === ui.currentTagId)?.name || '标签');
|
|
return '全部收藏';
|
|
});
|
|
|
|
async function doSearch() {
|
|
if (ui.searchQuery.trim()) {
|
|
doSearchFn(ui.searchQuery);
|
|
items.fetchItems();
|
|
}
|
|
}
|
|
|
|
async function sendAI() {
|
|
const q = aiQuery.value.trim();
|
|
if (!q || aiLoading.value) return;
|
|
aiLoading.value = true;
|
|
aiQuery.value = '';
|
|
try {
|
|
const res = await $fetch<any>('/api/ai/chat', { method: 'POST', body: { message: q } });
|
|
if (res.code === 0) {
|
|
aiAnswer.value = res.data.answer;
|
|
aiResults.value = res.data.items || [];
|
|
}
|
|
} finally {
|
|
aiLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function clearAiChat() {
|
|
aiAnswer.value = '';
|
|
aiResults.value = [];
|
|
}
|
|
|
|
function onScroll() {
|
|
const el = scrollArea.value;
|
|
if (!el) return;
|
|
const { scrollTop, scrollHeight, clientHeight } = el;
|
|
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
items.loadMore();
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// Only set default view if no specific filter is active (preserve sidebar navigation)
|
|
if (!ui.currentCategoryId && !ui.currentTagId && ui.currentView !== 'category' && ui.currentView !== 'tag') {
|
|
ui.navigate('inbox');
|
|
}
|
|
if (items.list.length === 0) {
|
|
await Promise.all([items.fetchItems(), categories.fetchCategories(), categories.fetchTags()]);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Main Header -->
|
|
<div class="main-header">
|
|
<div>
|
|
<div class="main-title">{{ pageTitle }}</div>
|
|
<div class="main-sub">{{ items.total }} 条收藏</div>
|
|
</div>
|
|
<div class="view-tabs">
|
|
<div class="view-tab" :class="{ active: ui.viewMode === 'grid' }" @click="ui.viewMode = 'grid'">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>卡片
|
|
</div>
|
|
<div class="view-tab" :class="{ active: ui.viewMode === 'list' }" @click="ui.viewMode = 'list'">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>列表
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Bar -->
|
|
<FilterBar v-model="ui.filterType" @update:modelValue="items.fetchItems()" />
|
|
|
|
<!-- Cards Area -->
|
|
<div class="cards-area" @scroll="onScroll" ref="scrollArea">
|
|
<div v-if="items.initialLoad || (items.loading && items.list.length === 0)" class="empty-state">
|
|
<div class="empty-state-icon">⏳</div>
|
|
<div>加载中...</div>
|
|
</div>
|
|
<div v-else-if="items.list.length === 0" class="empty-state">
|
|
<div class="empty-state-icon">🔍</div>
|
|
<div>没有找到匹配的收藏</div>
|
|
</div>
|
|
<div v-else class="cards-grid" :class="{ 'list-mode': ui.viewMode === 'list' }">
|
|
<ItemCard
|
|
v-for="item in items.list"
|
|
:key="item.id"
|
|
:item="item"
|
|
@select="ui.openDetail($event)"
|
|
@toggle-star="items.toggleStar($event)"
|
|
/>
|
|
</div>
|
|
<div v-if="items.hasMore" class="load-more" @click="items.loadMore()">
|
|
{{ items.loading ? '加载中...' : '加载更多' }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Strip -->
|
|
<div class="ai-section">
|
|
<div v-if="aiAnswer" class="ai-answer-box">
|
|
<div class="ai-answer-header">
|
|
<div class="ai-pulse"></div>
|
|
AI 回答
|
|
<button class="ai-clear" @click="clearAiChat">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="ai-answer-text" v-html="aiAnswer.replace(/\n/g, '<br>').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')" />
|
|
<div v-if="aiResults.length > 0" class="ai-results">
|
|
<div
|
|
v-for="r in aiResults"
|
|
:key="r.id"
|
|
class="ai-result-item"
|
|
@click="ui.openDetail(r.id)"
|
|
>
|
|
<div class="ai-result-type" :class="r.type">{{ r.type }}</div>
|
|
<div class="ai-result-title">{{ r.title }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<AiChatStrip v-model="aiQuery" @send="sendAI" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.main-header {
|
|
padding: 20px 24px 0;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
flex-shrink: 0;
|
|
}
|
|
.main-title {
|
|
font-family: var(--font-display);
|
|
font-size: 20px;
|
|
color: var(--text);
|
|
font-weight: 400;
|
|
}
|
|
.main-sub { font-size: 12px; color: var(--text3); margin-top: 2px; }
|
|
|
|
.view-tabs {
|
|
display: flex;
|
|
gap: 2px;
|
|
background: var(--bg3);
|
|
padding: 3px;
|
|
border-radius: 8px;
|
|
}
|
|
.view-tab {
|
|
padding: 5px 10px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
color: var(--text2);
|
|
transition: all 0.15s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.view-tab.active { background: var(--bg4); color: var(--text); }
|
|
.view-tab:hover:not(.active) { color: var(--text); }
|
|
|
|
.cards-area {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px 24px 24px;
|
|
}
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
.cards-grid.list-mode {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
gap: 12px;
|
|
color: var(--text3);
|
|
text-align: center;
|
|
}
|
|
.empty-state-icon { font-size: 40px; opacity: 0.5; }
|
|
.load-more {
|
|
text-align: center;
|
|
padding: 16px;
|
|
color: var(--accent);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
}
|
|
.ai-section { flex-shrink: 0; }
|
|
.ai-answer-box {
|
|
margin: 0 24px 0;
|
|
padding: 14px;
|
|
background: var(--bg2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
border-bottom: none;
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
.ai-answer-header {
|
|
font-size: 11px;
|
|
color: var(--accent);
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.ai-pulse {
|
|
width: 6px; height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: 0.5; transform: scale(0.8); }
|
|
}
|
|
.ai-clear {
|
|
margin-left: auto;
|
|
background: none;
|
|
border: none;
|
|
color: var(--text3);
|
|
cursor: pointer;
|
|
padding: 2px;
|
|
}
|
|
.ai-clear:hover { color: var(--text); }
|
|
.ai-answer-text {
|
|
font-size: 13px;
|
|
line-height: 1.7;
|
|
color: var(--text);
|
|
margin-bottom: 10px;
|
|
}
|
|
.ai-answer-text :deep(strong) { color: var(--accent); }
|
|
.ai-results { display: flex; flex-direction: column; gap: 4px; }
|
|
.ai-result-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 10px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
.ai-result-item:hover { background: var(--bg3); }
|
|
.ai-result-type {
|
|
padding: 1px 6px;
|
|
border-radius: 999px;
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
font-weight: 500;
|
|
flex-shrink: 0;
|
|
}
|
|
.ai-result-type.web { background: rgba(42,130,218,0.15); color: var(--blue); }
|
|
.ai-result-type.text { background: rgba(168,124,231,0.15); color: var(--purple); }
|
|
.ai-result-type.image { background: rgba(95,178,159,0.15); color: var(--teal); }
|
|
.ai-result-type.video { background: rgba(224,108,117,0.15); color: var(--red); }
|
|
.ai-result-type.file { background: rgba(200,169,126,0.15); color: var(--accent); }
|
|
.ai-result-title { font-size: 12px; color: var(--text); }
|
|
</style>
|
|
|