完全实现首页的三大框架基本功能
This commit is contained in:
@@ -1,354 +1,11 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 顶部导航 -->
|
||||
<el-header class="header">
|
||||
<div class="logo">Admin Dashboard</div>
|
||||
<div class="user-info">
|
||||
<el-avatar src="https://picsum.photos/200/200?random=100" />
|
||||
<span class="username">管理员</span>
|
||||
<el-dropdown>
|
||||
<el-button type="text">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||
<el-dropdown-item>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 数据概览卡片 -->
|
||||
<el-row :gutter="20" class="overview-cards">
|
||||
<el-col :span="6">
|
||||
<el-card class="info-card" hoverable>
|
||||
<div class="card-content">
|
||||
<div class="card-value">2.4k</div>
|
||||
<div class="card-label">总访问量</div>
|
||||
<div class="card-trend">
|
||||
<el-icon color="#27AE60"><ArrowUp /></el-icon>
|
||||
<span>12.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="info-card" hoverable>
|
||||
<div class="card-content">
|
||||
<div class="card-value">568</div>
|
||||
<div class="card-label">新增用户</div>
|
||||
<div class="card-trend">
|
||||
<el-icon color="#27AE60"><ArrowUp /></el-icon>
|
||||
<span>8.3%</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="info-card" hoverable>
|
||||
<div class="card-content">
|
||||
<div class="card-value">32%</div>
|
||||
<div class="card-label">转化率</div>
|
||||
<div class="card-trend">
|
||||
<el-icon color="#E74C3C"><ArrowDown /></el-icon>
|
||||
<span>2.1%</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="info-card" hoverable>
|
||||
<div class="card-content">
|
||||
<div class="card-value">¥12.8k</div>
|
||||
<div class="card-label">总收入</div>
|
||||
<div class="card-trend">
|
||||
<el-icon color="#27AE60"><ArrowUp /></el-icon>
|
||||
<span>15.7%</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<el-col :span="16">
|
||||
<el-card class="chart-card" hoverable>
|
||||
<div class="card-header">流量趋势</div>
|
||||
<div ref="lineChart" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="chart-card" hoverable>
|
||||
<div class="card-header">用户分布</div>
|
||||
<div ref="pieChart" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 快捷操作与最近活动 -->
|
||||
<el-row :gutter="20" class="actions-section">
|
||||
<el-col :span="8">
|
||||
<el-card class="action-card" hoverable>
|
||||
<div class="card-header">快捷操作</div>
|
||||
<div class="actions-list">
|
||||
<el-button type="primary" icon="Plus" class="action-btn">
|
||||
新建内容
|
||||
</el-button>
|
||||
<el-button type="success" icon="Upload" class="action-btn">
|
||||
数据导入
|
||||
</el-button>
|
||||
<el-button type="info" icon="Download" class="action-btn">
|
||||
数据导出
|
||||
</el-button>
|
||||
<el-button type="warning" icon="Setting" class="action-btn">
|
||||
系统设置
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-card class="activity-card" hoverable>
|
||||
<div class="card-header">最近活动</div>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(item, index) in activities"
|
||||
:key="index"
|
||||
:timestamp="item.time"
|
||||
placement="top"
|
||||
>
|
||||
<el-card>
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.desc }}</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import {
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Setting,
|
||||
Plus,
|
||||
Upload,
|
||||
Download,
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
// 模拟最近活动数据
|
||||
const activities = ref([
|
||||
{
|
||||
time: '2023-10-01 10:30',
|
||||
title: '系统版本更新',
|
||||
desc: '新增数据导出功能,优化图表加载速度',
|
||||
},
|
||||
{
|
||||
time: '2023-09-28 16:45',
|
||||
title: '用户量突破500',
|
||||
desc: '本月新增用户数达到历史新高,环比增长12.5%',
|
||||
},
|
||||
{
|
||||
time: '2023-09-20 09:15',
|
||||
title: '营销活动上线',
|
||||
desc: '开启“新用户注册送积分”活动,转化率提升8%',
|
||||
},
|
||||
]);
|
||||
|
||||
// 初始化折线图(流量趋势)
|
||||
const lineChart = ref(null);
|
||||
const initLineChart = () => {
|
||||
const chart = echarts.init(lineChart.value as unknown as HTMLElement);
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['10-01', '10-02', '10-03', '10-04', '10-05', '10-06', '10-07'],
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
data: [120, 132, 101, 134, 90, 230, 210],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#409EFF' },
|
||||
},
|
||||
],
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化饼图(用户分布)
|
||||
const pieChart = ref(null);
|
||||
const initPieChart = () => {
|
||||
if (pieChart.value) {
|
||||
const chart = echarts.init(pieChart.value as HTMLElement);
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 35, name: 'Web端' },
|
||||
{ value: 25, name: '移动端' },
|
||||
{ value: 20, name: '小程序' },
|
||||
{ value: 20, name: '其他' },
|
||||
],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
color: (params: any) => {
|
||||
const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C'];
|
||||
return colors[params.dataIndex];
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 页面挂载后初始化图表
|
||||
onMounted(() => {
|
||||
initLineChart();
|
||||
initPieChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 数据概览卡片 */
|
||||
.overview-cards {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 图表区域 */
|
||||
.charts-section {
|
||||
margin: 0 20px 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 快捷操作与活动 */
|
||||
.actions-section {
|
||||
margin: 0 20px 20px;
|
||||
}
|
||||
|
||||
.action-card,
|
||||
.activity-card {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.el-timeline {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.el-timeline-item__content {
|
||||
padding-top: 5px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -153,7 +153,7 @@ const stripHtml = (html: string) => {
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const reqData: ListArticleReq = { page: currentPage.value, size: pageSize.value,topic:"资源案例" };
|
||||
const reqData: ListArticleReq = { page: currentPage.value, size: pageSize.value,topic:"案例资源" };
|
||||
const response = await fetch(`${API_BASE_URL}/articles/getarticle`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
server:
|
||||
port: 8080
|
||||
allowed_origins: ["http://localhost:5173"]
|
||||
allowed_origins: ["*"]
|
||||
|
||||
oss:
|
||||
endpoint: "https://oss-cn-shenzhen.aliyuncs.com"
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
<el-carousel
|
||||
class="carousel-container"
|
||||
height="400px"
|
||||
indicator-position="bottom"
|
||||
autoplay
|
||||
interval="5000"
|
||||
arrow="always"
|
||||
>
|
||||
<el-carousel-item>
|
||||
@@ -100,11 +98,62 @@
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="news-section">
|
||||
<div class="section-header">
|
||||
<h2>案例资源 <span class="en-title">Resource</span></h2>
|
||||
<el-link class="more-btn">More</el-link>
|
||||
</div>
|
||||
|
||||
<div class="news-cards">
|
||||
<el-card
|
||||
class="news-card"
|
||||
v-for="(resource, index) in resourceList"
|
||||
:key="index"
|
||||
shadow="hover"
|
||||
:body-style="{ padding: '0px' }"
|
||||
>
|
||||
<img :src="resource.imageUrl" alt="案例资源图片" class="card-img">
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{{ resource.title }}</h3>
|
||||
<p class="card-desc">{{ resource.desc }}</p>
|
||||
<el-button type="text" class="detail-btn">
|
||||
<el-icon><ArrowRight /></el-icon> 详情
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="news-section">
|
||||
<div class="section-header">
|
||||
<h2>社区服务 <span class="en-title">Community</span></h2>
|
||||
<el-link class="more-btn">More</el-link>
|
||||
</div>
|
||||
|
||||
<div class="news-cards">
|
||||
<el-card
|
||||
class="news-card"
|
||||
v-for="(service, index) in communityList"
|
||||
:key="index"
|
||||
shadow="hover"
|
||||
:body-style="{ padding: '0px' }"
|
||||
>
|
||||
<img :src="service.imageUrl" alt="社区服务图片" class="card-img">
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{{ service.title }}</h3>
|
||||
<p class="card-desc">{{ service.desc }}</p>
|
||||
<el-button type="text" class="detail-btn">
|
||||
<el-icon><ArrowRight /></el-icon> 详情
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
// **修复点1:将所有用到的 Element Plus 组件一次性导入**
|
||||
import { ref, onMounted } from 'vue';
|
||||
import {
|
||||
ElMenu,
|
||||
ElMenuItem,
|
||||
@@ -115,37 +164,111 @@ import {
|
||||
ElButton,
|
||||
ElIcon
|
||||
} from 'element-plus';
|
||||
// **修复点2:从 @element-plus/icons-vue 导入图标**
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
|
||||
// 导航菜单激活项
|
||||
const activeIndex = ref('1');
|
||||
const handleSelect = (index: string) => {
|
||||
activeIndex.value = index;
|
||||
// 可添加导航跳转逻辑
|
||||
};
|
||||
|
||||
// 模拟新闻数据(实际项目可替换为接口请求)
|
||||
const newsList = ref([
|
||||
{
|
||||
imageUrl: 'https://picsum.photos/300/200?random=10', // 随机图片占位
|
||||
title: '健康设计与知识创新国际学术研讨会',
|
||||
desc: '探索健康设计的新路径,引领知识创新的新范式,共同描绘人类健康的未来图景。'
|
||||
},
|
||||
{
|
||||
imageUrl: 'https://picsum.photos/300/200?random=11',
|
||||
title: '第二届中国研究生“美丽中国”创新设计大赛',
|
||||
desc: '探索健康设计的新路径,引领知识创新的新范式,共同描绘人类健康的未来图景。'
|
||||
},
|
||||
{
|
||||
imageUrl: 'https://picsum.photos/300/200?random=12',
|
||||
title: '国家一流专业中期建设成果展',
|
||||
desc: '探索健康设计的新路径,引领知识创新的新范式,共同描绘人类健康的未来图景。'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
// --- Data Structures ---
|
||||
interface NewsItem {
|
||||
imageUrl: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface Article {
|
||||
id: number;
|
||||
title: string;
|
||||
cover: string;
|
||||
excerpt: string;
|
||||
}
|
||||
|
||||
// **修改点1: 为每个版块创建独立的响应式数组**
|
||||
const newsList = ref<NewsItem[]>([]);
|
||||
const resourceList = ref<NewsItem[]>([]);
|
||||
const communityList = ref<NewsItem[]>([]);
|
||||
|
||||
// --- Utility Function ---
|
||||
/**
|
||||
* @description 将HTML字符串转换为纯文本
|
||||
* @param htmlStr HTML字符串
|
||||
* @returns 纯文本字符串
|
||||
*/
|
||||
function stripHtml(htmlStr: string): string {
|
||||
if (!htmlStr) return '';
|
||||
const doc = new DOMParser().parseFromString(htmlStr, 'text/html');
|
||||
return doc.body.textContent || "";
|
||||
}
|
||||
|
||||
// **修改点2: 创建一个通用的、可复用的 fetch 函数**
|
||||
/**
|
||||
* @description 根据主题从后端获取文章列表
|
||||
* @param topic 文章主题 (e.g., "新闻", "案例资源")
|
||||
* @returns 处理过的文章列表
|
||||
*/
|
||||
const fetchArticlesByTopic = async (topic: string): Promise<NewsItem[]> => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/articles/getarticle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: 1,
|
||||
size: 3, // 只请求需要的前3条数据
|
||||
topic: topic
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error for topic ${topic}! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data && Array.isArray(data.Article_list)) {
|
||||
const articles: Article[] = data.Article_list;
|
||||
|
||||
return articles.map(article => {
|
||||
const plainTextDesc = stripHtml(article.excerpt);
|
||||
const truncatedDesc = plainTextDesc.length > 100
|
||||
? plainTextDesc.substring(0, 100) + '...'
|
||||
: plainTextDesc;
|
||||
|
||||
return {
|
||||
imageUrl: article.cover,
|
||||
title: article.title,
|
||||
desc: truncatedDesc,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
console.error(`API response for topic ${topic} is not in the expected format:`, data);
|
||||
return []; // 返回空数组以防错误
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch articles for topic ${topic}:`, error);
|
||||
return []; // 发生错误时返回空数组
|
||||
}
|
||||
};
|
||||
|
||||
// **修改点3: 在组件挂载时,并行获取所有版块的数据**
|
||||
onMounted(async () => {
|
||||
// 使用 Promise.all 来同时触发所有API请求,提高加载效率
|
||||
const [newsData, resourceData, communityData] = await Promise.all([
|
||||
fetchArticlesByTopic('新闻'),
|
||||
fetchArticlesByTopic('案例资源'),
|
||||
fetchArticlesByTopic('社区服务')
|
||||
]);
|
||||
|
||||
// 将获取到的数据赋值给对应的列表
|
||||
newsList.value = newsData;
|
||||
resourceList.value = resourceData;
|
||||
communityList.value = communityData;
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.header-with-carousel {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user