搜索、小助手推荐位

This commit is contained in:
2025-11-18 11:48:01 +08:00
parent 2ce3684711
commit 049b6f2cf3
18 changed files with 1280 additions and 23 deletions

View File

@@ -7,7 +7,7 @@
*/
import { api } from '@/apis';
import type { ResultDomain, Resource, ResourceSearchParams, PageParam, ResourceVO, UserCollection } from '@/types';
import type { ResultDomain, Resource, PageParam, ResourceVO, UserCollection, TaskItemVO } from '@/types';
/**
* 资源API服务
@@ -20,7 +20,7 @@ export const resourceApi = {
* @param filter 筛选条件
* @returns Promise<ResultDomain<Resource>>
*/
async getResourceList(filter?: ResourceSearchParams): Promise<ResultDomain<Resource>> {
async getResourceList(filter?: Resource): Promise<ResultDomain<Resource>> {
const response = await api.get<Resource>('/news/resources/list', filter);
return response.data;
},
@@ -31,7 +31,7 @@ export const resourceApi = {
* @param pageParam 分页参数
* @returns Promise<ResultDomain<Resource>>
*/
async getResourcePage(pageParam: PageParam, filter?: ResourceSearchParams): Promise<ResultDomain<Resource>> {
async getResourcePage(pageParam: PageParam, filter?: Resource): Promise<ResultDomain<Resource>> {
const response = await api.post<Resource>('/news/resources/page', {
pageParam,
filter,
@@ -45,7 +45,7 @@ export const resourceApi = {
* @param pageParam 分页参数
* @returns Promise<ResultDomain<ResourceVO>>
*/
async getResourcePageOrderByViewCount(pageParam: PageParam, filter?: ResourceSearchParams): Promise<ResultDomain<ResourceVO>> {
async getResourcePageOrderByViewCount(pageParam: PageParam, filter?: Resource): Promise<ResultDomain<ResourceVO>> {
const response = await api.post<ResourceVO>('/news/resources/page/view-count', {
pageParam,
filter,
@@ -218,6 +218,16 @@ export const resourceApi = {
const response = await api.get<Resource>('/news/resources/search', params);
return response.data;
},
/**
* 联合搜索文章和课程
* @param request 搜索请求参数包含pageParam和filter
* @returns Promise<ResultDomain<TaskItemVO>>
*/
async searchItems(request: { pageParam: PageParam; filter: Resource }): Promise<ResultDomain<TaskItemVO>> {
const response = await api.post<TaskItemVO>('/news/resources/search', request);
return response.data;
}
};

View File

@@ -208,10 +208,10 @@ function handleNavClick(menu: SysMenu) {
}
// 处理搜索
function handleSearch() {
if (searchKeyword.value.trim()) {
// 这里可以跳转到搜索页面或触发搜索功能
router.push(`/search?keyword=${encodeURIComponent(searchKeyword.value.trim())}`);
function handleSearch(keyword: string) {
if (keyword && keyword.trim()) {
// 跳转到搜索页面
router.push(`/search?keyword=${encodeURIComponent(keyword.trim())}`);
}
}

View File

@@ -52,5 +52,6 @@ export const APP_CONFIG = {
}
};
export const PUBLIC_IMG_PATH = 'http://localhost:8080/schoolNewsWeb/img';
export const PUBLIC_WEB_PATH = 'http://localhost:8080/schoolNewsWeb';
export default APP_CONFIG;

View File

@@ -331,6 +331,16 @@ export interface TaskItemVO extends LearningTask {
progress?: number;
/** 完成时间 */
completeTime?: string;
/** 封面图片(用于搜索结果展示) */
coverImage?: string;
/** 简介(用于搜索结果展示) */
summary?: string;
/** 作者(用于搜索结果展示) */
author?: string;
/** 浏览次数(用于搜索结果展示) */
viewCount?: number;
/** 发布时间(用于搜索结果展示) */
publishTime?: Date | string;
}
/**

View File

@@ -71,7 +71,7 @@
<img v-else src="@/assets/imgs/assistant.svg" alt="AI助手" class="welcome-avatar" />
</div>
<h2>你好我是{{ agentConfig?.name || 'AI助手' }}</h2>
<p>{{ agentConfig?.systemPrompt || '有什么可以帮助你的吗?' }}</p>
<AIRecommend />
</div>
<!-- 消息列表 -->
@@ -189,6 +189,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { chatApi, chatHistoryApi, aiAgentConfigApi, fileUploadApi } from '@/apis/ai';
import type { AiConversation, AiMessage, AiAgentConfig, AiUploadFile } from '@/types/ai';
import { AIRecommend } from '@/views/public/ai';
interface AIAgentProps {
agentId?: string;

View File

@@ -0,0 +1,271 @@
<template>
<div class="ai-recommend">
<div class="recommend-left">
<div class="recommend-title">
<span class="title-icon"></span>
<span class="title-text">智能推荐</span>
</div>
</div>
<div class="recommend-right">
<!-- Tab选项卡推荐top资源和思政资源5个 -->
<el-tabs v-model="activeName" class="recommend-tabs" @tab-click="handleClick">
<el-tab-pane label="推荐top资源" name="top"></el-tab-pane>
<el-tab-pane label="思政资源" name="ideological"></el-tab-pane>
</el-tabs>
<div class="recommend-list">
<div v-if="showData.length === 0" class="empty-state">
<span class="empty-icon">📚</span>
<p>暂无推荐内容</p>
</div>
<div v-else class="list-items">
<div v-for="(item, index) in showData" :key="item.id" class="recommend-item">
<span class="item-number">{{ index + 1 }}</span>
<a :href="buildUrl(item)" class="item-link" target="_blank">
<span class="item-title">{{ item.title }}</span>
<span class="item-arrow"></span>
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { PageParam, type ResourceRecommendVO } from '@/types'
import { resourceRecommendApi} from '@/apis/resource';
import { PUBLIC_WEB_PATH } from '@/config'
type TabName = 'top' | 'ideological'
const activeName = ref<TabName>('top')
const handleClick = (tab: any, event: Event) => {
console.log(tab, event)
loadData()
}
const showData = ref<ResourceRecommendVO[]>([])
const limit = 5
const tabMap: Record<TabName, number> = {top: 1, ideological: 2}
function buildUrl(item: ResourceRecommendVO) {
return `${PUBLIC_WEB_PATH}/article/show?articleId=${item.resourceID}`;
}
async function loadData(){
resourceRecommendApi.getRecommendsByType(tabMap[activeName.value], limit).then((res) => {
showData.value = res.dataList || []
})
}
onMounted(() => {
loadData().then(() => {
console.log()
})
})
</script>
<style lang="scss" scoped>
.ai-recommend {
width: 100%;
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: row;
gap: 16px;
padding: 16px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
.recommend-left {
min-width: 80px;
display: flex;
align-items: center;
justify-content: center;
.recommend-title {
writing-mode: vertical-lr;
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
letter-spacing: 4px;
.title-icon {
font-size: 24px;
animation: sparkle 2s ease-in-out infinite;
}
.title-text {
padding: 10px 0;
}
}
}
.recommend-right {
flex: 1;
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
:deep(.recommend-tabs) {
.el-tabs__header {
margin-bottom: 12px;
}
.el-tabs__nav-wrap::after {
background-color: #e8eaed;
}
.el-tabs__item {
font-size: 15px;
font-weight: 500;
color: #606266;
&:hover {
color: #409eff;
}
&.is-active {
color: #409eff;
font-weight: 600;
}
}
.el-tabs__active-bar {
height: 3px;
background: linear-gradient(90deg, #409eff 0%, #66b1ff 100%);
}
}
.recommend-list {
min-height: 180px;
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #909399;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
}
p {
font-size: 15px;
margin: 0;
}
}
.list-items {
display: flex;
flex-direction: column;
gap: 8px;
.recommend-item {
display: flex;
align-items: center;
padding: 10px 14px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid transparent;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
background: linear-gradient(90deg, #e3f2fd 0%, #f5f5f5 100%);
border-left-color: #409eff;
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
.item-arrow {
transform: translateX(4px);
opacity: 1;
}
}
.item-number {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
margin-right: 12px;
font-size: 12px;
font-weight: 600;
color: #909399;
background: white;
border-radius: 50%;
flex-shrink: 0;
}
.item-link {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
text-decoration: none;
color: #303133;
gap: 12px;
.item-title {
flex: 1;
font-size: 14px;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
color: #409eff;
}
}
.item-arrow {
font-size: 16px;
color: #409eff;
opacity: 0;
transition: all 0.3s ease;
flex-shrink: 0;
}
}
}
}
}
}
}
@keyframes sparkle {
0%, 100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
50% {
transform: scale(1.2) rotate(180deg);
opacity: 0.8;
}
}
// 响应式设计
@media (max-width: 768px) {
.ai-recommend {
flex-direction: column;
padding: 16px;
.recommend-left {
min-width: auto;
.recommend-title {
writing-mode: horizontal-tb;
flex-direction: row;
justify-content: center;
}
}
}
}
</style>

View File

@@ -1 +1,2 @@
export { default as AIAgent} from './AIAgent.vue';
export { default as AIAgent} from './AIAgent.vue';
export { default as AIRecommend} from './AIRecommend.vue';

View File

@@ -0,0 +1,769 @@
<template>
<div class="search-view">
<!-- 页面头部 -->
<div class="search-header">
<div class="header-content">
<div class="header-left">
<button class="back-button" @click="goBack">
<el-icon>
<ArrowLeft />
</el-icon>
<span>返回</span>
</button>
<div class="header-info">
<h1 class="page-title">
<el-icon class="title-icon">
<Search />
</el-icon>
搜索结果
</h1>
<p class="page-desc" v-if="searchKeyword">
搜索关键词: <span class="keyword-text">{{ searchKeyword }}</span>
</p>
</div>
</div>
<!-- 搜索框 -->
<div class="header-search">
<el-input
v-model="localSearchKeyword"
placeholder="搜索文章和课程内容"
class="search-input"
@keyup.enter="handleSearch"
>
<template #suffix>
<el-button
type="primary"
:icon="Search"
@click="handleSearch"
class="search-button"
>
搜索
</el-button>
</template>
</el-input>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-bar" v-if="!loading && (articles.length > 0 || courses.length > 0)">
<div class="stat-item">
<span class="stat-label">共找到</span>
<span class="stat-value">{{ total }}</span>
<span class="stat-label">条结果</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-label">文章</span>
<span class="stat-value">{{ articles.length }}</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-label">课程</span>
<span class="stat-value">{{ courses.length }}</span>
</div>
</div>
</div>
<!-- 搜索结果 -->
<div class="search-results" v-loading="loading">
<!-- 空状态 -->
<div v-if="!loading && total === 0" class="empty-state">
<el-icon class="empty-icon">
<DocumentDelete />
</el-icon>
<h3>未找到相关内容</h3>
<p>换个关键词试试吧</p>
</div>
<!-- 文章结果 -->
<div v-if="articles.length > 0" class="result-section">
<div class="section-header">
<h2 class="section-title">
<el-icon>
<Document />
</el-icon>
文章 ({{ articles.length }})
</h2>
</div>
<div class="articles-grid">
<div
v-for="article in articles"
:key="article.resourceID"
class="article-card"
@click="handleArticleClick(article)"
>
<!-- 文章封面 -->
<div class="article-cover">
<img
v-if="article.coverImage"
:src="FILE_DOWNLOAD_URL + article.coverImage"
:alt="article.resourceName"
/>
<div v-else class="cover-placeholder">
<el-icon>
<Document />
</el-icon>
</div>
<div class="cover-overlay">
<span class="view-button">查看详情</span>
</div>
</div>
<!-- 文章信息 -->
<div class="article-info">
<h3 class="article-title" :title="article.resourceName">
{{ article.resourceName }}
</h3>
<!-- 简介 -->
<p class="article-summary">{{ article.summary || '暂无简介' }}</p>
<!-- 底部元信息 -->
<div class="article-meta">
<div class="meta-item">
<el-icon class="meta-icon">
<User />
</el-icon>
<span>{{ article.author || '未知' }}</span>
</div>
<div class="meta-item">
<el-icon class="meta-icon">
<View />
</el-icon>
<span>{{ formatNumber(article.viewCount) }}</span>
</div>
<div class="meta-item" v-if="article.publishTime">
<el-icon class="meta-icon">
<Clock />
</el-icon>
<span>{{ formatDate(article.publishTime) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 课程结果 -->
<div v-if="courses.length > 0" class="result-section">
<div class="section-header">
<h2 class="section-title">
<el-icon>
<Reading />
</el-icon>
课程 ({{ courses.length }})
</h2>
</div>
<div class="courses-grid">
<div
v-for="course in courses"
:key="course.courseID"
class="course-card"
@click="handleCourseClick(course)"
>
<!-- 课程封面 -->
<div class="course-cover">
<img
v-if="course.coverImage"
:src="FILE_DOWNLOAD_URL + course.coverImage"
:alt="course.courseName"
/>
<div v-else class="cover-placeholder">
<el-icon>
<Reading />
</el-icon>
</div>
<div class="cover-overlay">
<span class="view-button">查看课程</span>
</div>
</div>
<!-- 课程信息 -->
<div class="course-info">
<h3 class="course-title" :title="course.courseName">
{{ course.courseName }}
</h3>
<!-- 简介 -->
<p class="course-summary">{{ course.summary || '暂无简介' }}</p>
<!-- 底部元信息 -->
<div class="course-meta">
<div class="meta-item">
<el-icon class="meta-icon">
<User />
</el-icon>
<span>{{ course.author || '未知' }}</span>
</div>
<div class="meta-item">
<el-icon class="meta-icon">
<View />
</el-icon>
<span>{{ formatNumber(course.viewCount) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="total > pageSize" class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import {
ArrowLeft,
Search,
Document,
Reading,
User,
View,
Clock,
DocumentDelete,
} from '@element-plus/icons-vue';
import { resourceApi } from '@/apis/resource';
import { FILE_DOWNLOAD_URL } from '@/config';
import type { TaskItemVO } from '@/types/study';
const router = useRouter();
const route = useRoute();
// 响应式数据
const loading = ref(false);
const searchKeyword = ref('');
const localSearchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(20);
const total = ref(0);
const searchResults = ref<TaskItemVO[]>([]);
// 计算属性:分离文章和课程
const articles = computed(() => {
return searchResults.value.filter(item => item.itemType === 1);
});
const courses = computed(() => {
return searchResults.value.filter(item => item.itemType === 2);
});
/**
* 加载搜索结果
*/
async function loadSearchResults() {
if (!searchKeyword.value.trim()) {
return;
}
loading.value = true;
try {
const result = await resourceApi.searchItems({
pageParam: {
pageNumber: currentPage.value,
pageSize: pageSize.value,
},
filter: {
title: searchKeyword.value.trim(),
status: 1, // 只查询已发布的内容
},
});
if (result.success && result.dataList) {
searchResults.value = result.dataList;
total.value = result.dataList.length;
} else {
searchResults.value = [];
total.value = 0;
}
} catch (error) {
console.error('搜索失败:', error);
ElMessage.error('搜索失败,请稍后重试');
searchResults.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
/**
* 执行搜索
*/
function handleSearch() {
if (!localSearchKeyword.value.trim()) {
ElMessage.warning('请输入搜索关键词');
return;
}
searchKeyword.value = localSearchKeyword.value;
currentPage.value = 1;
// 更新URL
router.push({
path: '/search',
query: { keyword: searchKeyword.value },
});
loadSearchResults();
}
/**
* 页码变化
*/
function handlePageChange(page: number) {
currentPage.value = page;
loadSearchResults();
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/**
* 每页数量变化
*/
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
loadSearchResults();
}
/**
* 点击文章
*/
function handleArticleClick(article: TaskItemVO) {
router.push(`/article/show?articleId=${article.resourceID}`);
}
/**
* 点击课程
*/
function handleCourseClick(course: TaskItemVO) {
router.push(`/study-plan/course-detail?courseId=${course.courseID}`);
}
/**
* 返回上一页
*/
function goBack() {
router.back();
}
/**
* 格式化数字
*/
function formatNumber(num?: number): string {
if (!num) return '0';
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w';
}
return num.toString();
}
/**
* 格式化日期
*/
function formatDate(date?: Date | string): string {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 监听路由变化
watch(
() => route.query.keyword,
(newKeyword) => {
if (newKeyword && typeof newKeyword === 'string') {
searchKeyword.value = newKeyword;
localSearchKeyword.value = newKeyword;
loadSearchResults();
}
}
);
// 组件挂载
onMounted(() => {
const keyword = route.query.keyword;
if (keyword && typeof keyword === 'string') {
searchKeyword.value = keyword;
localSearchKeyword.value = keyword;
loadSearchResults();
}
});
</script>
<style lang="scss" scoped>
.search-view {
min-height: 100vh;
background: #f5f7fa;
}
.search-header {
background: white;
margin-bottom: 24px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
}
.header-content {
padding: 24px 32px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 32px;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
}
.back-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #f5f7fa;
border: none;
border-radius: 4px;
color: #606266;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
&:hover {
background: #e4e7ed;
color: #409eff;
}
}
.header-info {
flex: 1;
}
.page-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 24px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
.title-icon {
font-size: 28px;
color: #409eff;
}
}
.page-desc {
font-size: 14px;
color: #909399;
margin: 0;
.keyword-text {
color: #409eff;
font-weight: 600;
}
}
.header-search {
width: 400px;
.search-input {
:deep(.el-input__wrapper) {
border-radius: 24px;
padding-right: 0;
}
:deep(.el-input__suffix) {
margin-right: 4px;
}
}
.search-button {
border-radius: 20px;
padding: 8px 24px;
}
}
.stats-bar {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 32px;
background: #f5f7fa;
border-top: 1px solid #e4e7ed;
}
.stat-item {
display: flex;
align-items: center;
gap: 8px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #409eff;
}
.stat-divider {
width: 1px;
height: 16px;
background: #dcdfe6;
}
.search-results {
padding: 0 32px 32px;
min-height: 400px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
.empty-icon {
font-size: 80px;
color: #dcdfe6;
margin-bottom: 16px;
}
h3 {
font-size: 18px;
color: #606266;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #909399;
margin: 0;
}
}
.result-section {
margin-bottom: 48px;
&:last-child {
margin-bottom: 0;
}
}
.section-header {
margin-bottom: 24px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 20px;
font-weight: 600;
color: #303133;
margin: 0;
.el-icon {
font-size: 24px;
color: #409eff;
}
}
.articles-grid,
.courses-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
.article-card,
.course-card {
background: white;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 21, 41, 0.12);
.cover-overlay {
opacity: 1;
}
}
}
.article-cover,
.course-cover {
position: relative;
width: 100%;
height: 180px;
overflow: hidden;
background: #f5f7fa;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.el-icon {
font-size: 64px;
color: rgba(255, 255, 255, 0.8);
}
}
.cover-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
.view-button {
padding: 8px 24px;
background: white;
color: #409eff;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
}
}
}
.article-info,
.course-info {
padding: 16px;
}
.article-title,
.course-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 12px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
min-height: 44.8px;
}
.article-summary,
.course-summary {
font-size: 14px;
color: #909399;
margin: 0 0 12px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
min-height: 42px;
}
.article-meta,
.course-meta {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #909399;
.meta-icon {
font-size: 14px;
}
}
.pagination {
display: flex;
justify-content: center;
margin-top: 32px;
padding: 24px 0;
}
@media (max-width: 1200px) {
.header-content {
flex-direction: column;
align-items: flex-start;
}
.header-search {
width: 100%;
}
.articles-grid,
.courses-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
@media (max-width: 768px) {
.header-content {
padding: 16px 20px;
}
.search-results {
padding: 0 20px 20px;
}
.articles-grid,
.courses-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -45,7 +45,7 @@
import { ref, watch, onMounted, nextTick } from 'vue';
import { resourceApi } from '@/apis/resource';
import { FILE_DOWNLOAD_URL } from '@/config';
import type { Resource, ResourceSearchParams } from '@/types/resource';
import type { Resource } from '@/types/resource';
import type { PageParam } from '@/types';
import defaultArticleImg from '@/assets/imgs/article-default.png';
@@ -83,9 +83,9 @@ async function loadResources() {
loading.value = true;
try {
const filter: ResourceSearchParams = {
const filter: Resource = {
tagID: props.tagID,
keyword: props.searchKeyword,
title: props.searchKeyword,
status: 1 // 只加载已发布的
};

View File

@@ -17,20 +17,20 @@
<div class="records-table" v-loading="tableLoading">
<h3>学习记录明细</h3>
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column prop="resourceTitle" label="资源标题" width="200" />
<el-table-column prop="resourceTypeName" label="类型" width="100" />
<el-table-column label="学习时长" width="120">
<el-table-column prop="resourceTitle" label="资源标题"/>
<el-table-column prop="resourceTypeName" label="类型" />
<el-table-column label="学习时长">
<template #default="{ row }">
{{ formatDuration(row.totalDuration) }}
</template>
</el-table-column>
<el-table-column prop="learnCount" label="学习次数" width="100" />
<el-table-column prop="statDate" label="统计日期" width="150">
<el-table-column prop="learnCount" label="学习次数" />
<el-table-column prop="statDate" label="统计日期">
<template #default="{ row }">
{{ row.statDate ? new Date(row.statDate).toLocaleDateString('zh-CN') : '' }}
</template>
</el-table-column>
<el-table-column label="完成状态" width="100">
<el-table-column label="完成状态">
<template #default="{ row }">
<el-tag :type="row.isComplete ? 'success' : 'info'">{{ row.isComplete ? '已完成' : '学习中' }}</el-tag>
</template>
@@ -38,6 +38,7 @@
</el-table>
<el-pagination
class="pagination-container"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalElements"