Files
Jiao77-Blog/src/components/MicroSidebar.vue
Jiao77 0961bbd1b7 feat: 添加微语功能
- 新增微语页面,类似 Twitter/QQ 空间的短内容发布平台
- 添加 GitHub 风格热力图组件展示发布活动
- 支持发布微语、图片上传、标签、Emoji
- 支持点赞、评论功能
- 右侧栏显示统计数据和热门标签
- 支持按标签筛选微语
- 后端新增微语相关 API(CRUD、点赞、评论、标签)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 16:56:48 +08:00

191 lines
5.4 KiB
Vue

<template>
<div class="space-y-6">
<!-- 热力图 -->
<HeatmapCalendar
title="发布活动"
:data="heatmapData"
:year="year"
color-scheme="green"
/>
<!-- 统计卡片 -->
<div class="card">
<h3 class="text-lg font-semibold mb-4">统计</h3>
<div v-if="loading" class="text-center py-4">
<div class="animate-spin w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full mx-auto"></div>
</div>
<div v-else class="grid grid-cols-2 gap-4">
<div class="text-center p-4 bg-muted/50 rounded-lg">
<div class="text-2xl font-bold text-primary-500">{{ stats.total_micros || 0 }}</div>
<div class="text-sm text-foreground/60">总微语</div>
</div>
<div class="text-center p-4 bg-muted/50 rounded-lg">
<div class="text-2xl font-bold text-primary-500">{{ stats.month_micros || 0 }}</div>
<div class="text-sm text-foreground/60">本月发布</div>
</div>
<div class="text-center p-4 bg-muted/50 rounded-lg">
<div class="text-2xl font-bold text-primary-500">{{ formatNumber(stats.total_likes) }}</div>
<div class="text-sm text-foreground/60">总点赞</div>
</div>
<div class="text-center p-4 bg-muted/50 rounded-lg">
<div class="text-2xl font-bold text-primary-500">{{ stats.total_comments || 0 }}</div>
<div class="text-sm text-foreground/60">总评论</div>
</div>
</div>
</div>
<!-- 标签云 -->
<div class="card">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">热门标签</h3>
<button
v-if="currentTag"
@click="clearTagFilter"
class="text-xs text-primary-500 hover:text-primary-600"
>
清除筛选
</button>
</div>
<div v-if="loadingTags" class="text-center py-2">
<div class="animate-spin w-5 h-5 border-2 border-primary-500 border-t-transparent rounded-full mx-auto"></div>
</div>
<div v-else-if="tags.length === 0" class="text-center py-2 text-foreground/40 text-sm">
暂无标签
</div>
<div v-else class="flex flex-wrap gap-2">
<span
v-for="tag in tags"
:key="tag.name"
class="inline-block px-2 py-1 text-xs rounded-full cursor-pointer transition-colors"
:class="currentTag === tag.name
? 'bg-primary-500 text-white'
: 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-800'"
@click="filterByTag(tag.name)"
>
#{{ tag.name }} ({{ tag.count }})
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import HeatmapCalendar from './HeatmapCalendar.vue';
interface Props {
apiBaseUrl?: string;
}
const props = withDefaults(defineProps<Props>(), {
apiBaseUrl: 'http://localhost:8080/api',
});
const year = new Date().getFullYear();
const loading = ref(true);
const loadingTags = ref(true);
const heatmapData = ref<Record<string, number>>({});
const stats = ref({
total_micros: 0,
month_micros: 0,
total_likes: 0,
total_comments: 0,
});
const tags = ref<{ name: string; count: number }[]>([]);
const currentTag = ref('');
// 格式化数字
function formatNumber(num: number): string {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return String(num);
}
// 加载热力图数据
async function loadHeatmap() {
try {
const response = await fetch(`${props.apiBaseUrl}/micro/heatmap?year=${year}`);
if (response.ok) {
const data = await response.json();
heatmapData.value = data.data || {};
}
} catch (error) {
console.error('Failed to load heatmap:', error);
}
}
// 加载统计数据
async function loadStats() {
try {
const response = await fetch(`${props.apiBaseUrl}/micro/stats`);
if (response.ok) {
const data = await response.json();
stats.value = data;
}
} catch (error) {
console.error('Failed to load stats:', error);
} finally {
loading.value = false;
}
}
// 加载标签
async function loadTags() {
try {
const response = await fetch(`${props.apiBaseUrl}/micro/tags`);
if (response.ok) {
const data = await response.json();
tags.value = data.tags || [];
}
} catch (error) {
console.error('Failed to load tags:', error);
} finally {
loadingTags.value = false;
}
}
// 按标签筛选
function filterByTag(tag: string) {
currentTag.value = tag;
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('tag-filter', { detail: tag }));
}
}
// 清除筛选
function clearTagFilter() {
currentTag.value = '';
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('tag-filter', { detail: '' }));
}
}
// 刷新所有数据
function refresh() {
loadHeatmap();
loadStats();
loadTags();
}
// 暴露方法给父组件
defineExpose({ refresh });
onMounted(() => {
loadHeatmap();
loadStats();
loadTags();
// 监听刷新事件
window.addEventListener('refresh-sidebar', refresh);
// 监听标签筛选事件(同步当前选中状态)
window.addEventListener('tag-filter', ((e: CustomEvent) => {
currentTag.value = e.detail || '';
}) as EventListener);
});
onUnmounted(() => {
window.removeEventListener('refresh-sidebar', refresh);
});
</script>