Browse Source
feat(component): add ArticleMetaBar with reading-time estimate and meta icons on article pages
main
feat(component): add ArticleMetaBar with reading-time estimate and meta icons on article pages
main
4 changed files with 107 additions and 22 deletions
-
19src/components/ArticleCard.vue
-
74src/components/ArticleMetaBar.vue
-
12src/utils/readingTime.ts
-
24src/views/ArticleDetailView.vue
@ -0,0 +1,74 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed } from 'vue' |
||||
|
import { estimateReadingTime, formatDay } from '@/utils/readingTime' |
||||
|
|
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
createdAt?: string |
||||
|
viewCount?: number |
||||
|
content?: string |
||||
|
mode?: 'card' | 'detail' |
||||
|
}>(), |
||||
|
{ createdAt: '', viewCount: 0, content: '', mode: 'card' } |
||||
|
) |
||||
|
|
||||
|
const day = computed(() => (props.createdAt ? formatDay(props.createdAt) : '')) |
||||
|
const duration = computed(() => estimateReadingTime(props.content ?? '')) |
||||
|
const showView = computed(() => props.mode === 'detail') |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<span class="meta-bar" :class="`meta-bar--${mode}`"> |
||||
|
<span v-if="day" class="meta-item"> |
||||
|
<svg class="meta-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
||||
|
<rect x="2" y="3" width="12" height="11" rx="1.5" /> |
||||
|
<line x1="2" y1="7" x2="14" y2="7" /> |
||||
|
<line x1="5.5" y1="1.5" x2="5.5" y2="4.5" /> |
||||
|
<line x1="10.5" y1="1.5" x2="10.5" y2="4.5" /> |
||||
|
</svg> |
||||
|
<span>{{ day }}</span> |
||||
|
</span> |
||||
|
<span v-if="showView" class="meta-item"> |
||||
|
<svg class="meta-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
||||
|
<path d="M1.5 8C3 5.1 5.3 3.5 8 3.5S13 5.1 14.5 8C13 10.9 10.7 12.5 8 12.5S3 10.9 1.5 8Z" /> |
||||
|
<circle cx="8" cy="8" r="2.1" /> |
||||
|
</svg> |
||||
|
<span class="tnum">{{ viewCount }}</span> |
||||
|
</span> |
||||
|
<span v-if="duration" class="meta-item"> |
||||
|
<svg class="meta-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
||||
|
<circle cx="8" cy="8" r="6" /> |
||||
|
<path d="M8 5V8L10 9.2" /> |
||||
|
</svg> |
||||
|
<span>{{ duration }}</span> |
||||
|
</span> |
||||
|
</span> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.meta-bar { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 14px; |
||||
|
color: var(--ac-text-disabled); |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.meta-item { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.meta-icon { |
||||
|
width: 14px; |
||||
|
height: 14px; |
||||
|
opacity: 0.85; |
||||
|
} |
||||
|
|
||||
|
.meta-bar--card .meta-icon { |
||||
|
width: 13px; |
||||
|
height: 13px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
export function estimateReadingTime(content: string = ''): string { |
||||
|
const noWhitespace = content.replace(/\s/g, '').length |
||||
|
const minutes = Math.ceil(noWhitespace / 400) |
||||
|
return `${minutes} 分钟` |
||||
|
} |
||||
|
|
||||
|
export function formatDay(iso: string): string { |
||||
|
const d = new Date(iso) |
||||
|
if (isNaN(d.getTime())) return iso |
||||
|
const pad = (n: number) => String(n).padStart(2, '0') |
||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue