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.
 
 
 
 

322 lines
11 KiB

<template>
<div class="detail-panel" :class="{ open: !!item }">
<template v-if="item">
<div class="detail-header">
<div class="detail-header-title">收藏详情</div>
<button class="detail-close" @click="$emit('close')">
<svg width="14" height="14" 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="detail-body">
<div class="detail-thumb" :class="typeBgClass">{{ typeEmoji }}</div>
<div class="detail-title">{{ item.title }}</div>
<div class="detail-source">
<div :style="{ width: '14px', height: '14px', borderRadius: '4px', background: typeAccent + '22', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '9px', color: typeAccent }"></div>
{{ item.sourceHost || '未知来源' }} · {{ formatDate(item.createdAt) }}
<span style="margin-left:auto"><StarRating :rating="item.rating || 0" /></span>
</div>
<div class="detail-divider"></div>
<div class="detail-section-label">
AI 摘要
<button v-if="!item.aiSummary" class="btn-gen-summary" @click="$emit('generateSummary', item.id)">生成</button>
</div>
<div class="detail-summary" :class="{ loading: summaryLoading }">{{ summaryLoading ? '正在生成摘要...' : (item.aiSummary || '暂无 AI 摘要') }}</div>
<div class="detail-section-label">标签</div>
<div class="detail-tags">
<span v-for="t in item.tags" :key="t.id" class="detail-tag" :style="{ background: tagBg(t.name), color: tagColor(t.name) }">#{{ t.name }}</span>
<input
v-if="showTagInput"
ref="tagInput"
v-model="newTagName"
class="detail-tag-input"
placeholder="输入标签名"
@keydown.enter="addTag"
@blur="addTag"
/>
<span v-else class="detail-tag" style="background:var(--bg3);color:var(--text3);cursor:pointer" @click="openTagInput">+ 添加</span>
</div>
<div class="detail-divider"></div>
<div class="detail-section-label">我的备注</div>
<textarea class="note-area" placeholder="写下你的想法..." v-model="noteText" @blur="saveNote"></textarea>
<div class="detail-divider"></div>
<div class="detail-section-label">相关收藏推荐</div>
<div
v-for="r in relatedItems"
:key="r.id"
class="related-item"
@click="$emit('select', r.id)"
>
<div class="related-thumb" :class="typeMap[r.type]?.bg">{{ typeMap[r.type]?.emoji }}</div>
<div class="related-info">
<div class="related-title">{{ r.title }}</div>
<div class="related-tag">{{ r.tags?.[0]?.name }}</div>
</div>
</div>
</div>
<div class="detail-actions">
<button class="btn-action primary" @click="$emit('writeArticle', item.id)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
写文章
</button>
<button class="btn-action" @click="$emit('copyLink', item)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
复制链接
</button>
<button class="btn-action" @click="$emit('delete', item.id)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
删除
</button>
</div>
</template>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ item: any; relatedItems: any[]; summaryLoading?: boolean }>();
const emit = defineEmits<{ close: []; select: [id: number]; writeArticle: [id: number]; copyLink: [item: any]; delete: [id: number]; generateSummary: [id: number]; saveNote: [itemId: number, note: string]; addTag: [itemId: number, tagName: string] }>();
const noteText = ref('');
const showTagInput = ref(false);
const newTagName = ref('');
const tagInput = ref<HTMLInputElement>();
watch(() => props.item, (v) => {
noteText.value = v?.note || '';
showTagInput.value = false;
newTagName.value = '';
});
function saveNote() {
if (!props.item || noteText.value === (props.item.note || '')) return;
emit('saveNote', props.item.id, noteText.value);
}
function openTagInput() {
showTagInput.value = true;
nextTick(() => tagInput.value?.focus());
}
function addTag() {
const name = newTagName.value.trim();
if (!name || !props.item) return;
emit('addTag', props.item.id, name);
newTagName.value = '';
showTagInput.value = false;
}
const typeMap: Record<string, any> = {
web: { label: '网页', emoji: '🌐', bg: 'bg-web', badge: 'badge-web', accent: 'var(--blue)' },
text: { label: '文本', emoji: '📝', bg: 'bg-text', badge: 'badge-text', accent: 'var(--purple)' },
image: { label: '图片', emoji: '📷', bg: 'bg-img', badge: 'badge-img', accent: 'var(--teal)' },
video: { label: '视频', emoji: '▶️', bg: 'bg-vid', badge: 'badge-vid', accent: 'var(--red)' },
file: { label: '文档', emoji: '📄', bg: 'bg-doc', badge: 'badge-doc', accent: 'var(--accent)' },
};
const typeData = computed(() => typeMap[props.item?.type] || typeMap.web);
const typeBgClass = computed(() => typeData.value.bg);
const typeEmoji = computed(() => typeData.value.emoji);
const typeAccent = computed(() => typeData.value.accent);
function formatDate(ts: number) {
if (!ts) return '';
const d = new Date(ts);
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function tagBg(name: string) {
const m: Record<string, string> = { '灵感': 'var(--purple-bg)', '设计': 'var(--teal-bg)', 'AI工具': 'var(--blue-bg)', '值得深读': 'var(--accent-bg)', '参考资料': 'rgba(93,184,160,0.12)' };
return m[name] || 'var(--bg4)';
}
function tagColor(name: string) {
const m: Record<string, string> = { '灵感': 'var(--purple)', '设计': 'var(--teal)', 'AI工具': 'var(--blue)', '值得深读': 'var(--accent)', '参考资料': 'var(--teal)' };
return m[name] || 'var(--text2)';
}
</script>
<style scoped>
.detail-panel {
width: 380px;
background: var(--bg2);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
transform: translateX(100%);
transition: transform 0.25s ease;
position: absolute;
right: 0;
top: 0;
bottom: 0;
}
.detail-panel.open { transform: translateX(0); }
.detail-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-header-title { font-size: 13px; font-weight: 500; color: var(--text2); }
.detail-close {
width: 26px; height: 26px; border-radius: 6px;
background: var(--bg3); border: none; cursor: pointer;
color: var(--text2); display: flex; align-items: center; justify-content: center;
transition: background 0.15s;
}
.detail-close:hover { background: var(--bg4); color: var(--text); }
.detail-body { flex: 1; overflow-y: auto; padding: 20px; }
.detail-body::-webkit-scrollbar { width: 0; }
.detail-thumb {
height: 160px;
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
}
.detail-title {
font-family: var(--font-display);
font-size: 18px;
line-height: 1.4;
color: var(--text);
margin-bottom: 10px;
}
.detail-source {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text3);
margin-bottom: 14px;
}
.detail-divider { height: 1px; background: var(--border); margin: 16px 0; }
.detail-section-label {
font-size: 10px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text3);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.btn-gen-summary {
font-size: 10px;
background: var(--accent-bg, rgba(200,169,126,0.12));
border: 1px solid rgba(200,169,126,0.25);
color: var(--accent);
padding: 2px 8px;
border-radius: 999px;
cursor: pointer;
letter-spacing: 0;
text-transform: none;
transition: background 0.15s;
}
.btn-gen-summary:hover { background: rgba(200,169,126,0.2); }
.detail-summary {
font-size: 13px;
color: var(--text2);
line-height: 1.7;
background: var(--bg3);
border-radius: var(--radius);
padding: 12px 14px;
border-left: 2px solid var(--accent);
margin-bottom: 14px;
}
.detail-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
.detail-tag {
font-size: 12px;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.15s;
}
.detail-tag:hover { opacity: 0.8; }
.detail-tag-input {
font-size: 12px;
padding: 4px 10px;
border-radius: 6px;
background: var(--bg3);
border: 1px solid var(--accent);
color: var(--text);
outline: none;
width: 100px;
font-family: var(--font-body);
}
.note-area {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 12px;
color: var(--text);
font-family: var(--font-body);
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
min-height: 80px;
transition: border-color 0.2s;
}
.note-area:focus { border-color: var(--border2); }
.detail-actions {
padding: 12px 20px;
border-top: 1px solid var(--border);
display: flex;
gap: 8px;
}
.btn-action {
flex: 1;
padding: 8px 12px;
border-radius: 8px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
background: var(--bg3);
color: var(--text2);
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-action:hover { border-color: var(--border2); color: var(--text); }
.btn-action.primary { background: var(--accent-bg); border-color: rgba(200, 169, 126, 0.3); color: var(--accent); }
.btn-action.primary:hover { background: rgba(200, 169, 126, 0.2); }
.related-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
margin-bottom: 6px;
}
.related-item:hover { background: var(--bg3); }
.related-thumb {
width: 32px; height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.related-info { flex: 1; min-width: 0; }
.related-title {
font-size: 12px;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.related-tag { font-size: 11px; color: var(--text3); }
</style>