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