feat: 添加微语功能
- 新增微语页面,类似 Twitter/QQ 空间的短内容发布平台 - 添加 GitHub 风格热力图组件展示发布活动 - 支持发布微语、图片上传、标签、Emoji - 支持点赞、评论功能 - 右侧栏显示统计数据和热门标签 - 支持按标签筛选微语 - 后端新增微语相关 API(CRUD、点赞、评论、标签) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
191
src/components/MicroSidebar.vue
Normal file
191
src/components/MicroSidebar.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user