feat(试卷): 添加试卷收藏功能及界面展示

实现试卷收藏功能,包括本地存储管理收藏状态、添加/移除收藏操作,并在试卷列表页面展示收藏状态。新增收藏图标交互样式,支持按收藏状态排序试卷列表。当用户操作收藏时显示相应的提示信息。
This commit is contained in:
CGW406
2026-04-25 09:27:06 +08:00
parent 0547e0f4fd
commit 64beb23458
2 changed files with 189 additions and 12 deletions
@@ -0,0 +1,125 @@
const FAVORITE_KEY = 'xzs_favorite_papers'
const FAVORITE_LIMIT = 100
function safeParse (str) {
try {
return JSON.parse(str)
} catch (e) {
return []
}
}
function safeStringify (obj) {
try {
return JSON.stringify(obj)
} catch (e) {
return '[]'
}
}
export function getFavorites () {
try {
const data = window.localStorage.getItem(FAVORITE_KEY)
if (!data) return []
const favorites = safeParse(data)
return Array.isArray(favorites) ? favorites : []
} catch (e) {
return []
}
}
export function isFavorite (paperId) {
const id = Number(paperId)
if (isNaN(id)) return false
const favorites = getFavorites()
return favorites.some(item => item.id === id)
}
export function addFavorite (paperId, paperInfo = {}) {
const id = Number(paperId)
if (isNaN(id)) return false
if (isFavorite(id)) return true
const favorites = getFavorites()
const newItem = {
id,
name: paperInfo.name || '',
subjectId: paperInfo.subjectId || null,
createdAt: Date.now()
}
favorites.unshift(newItem)
if (favorites.length > FAVORITE_LIMIT) {
favorites.splice(FAVORITE_LIMIT)
}
try {
window.localStorage.setItem(FAVORITE_KEY, safeStringify(favorites))
return true
} catch (e) {
return false
}
}
export function removeFavorite (paperId) {
const id = Number(paperId)
if (isNaN(id)) return false
const favorites = getFavorites()
const newFavorites = favorites.filter(item => item.id !== id)
try {
window.localStorage.setItem(FAVORITE_KEY, safeStringify(newFavorites))
return true
} catch (e) {
return false
}
}
export function toggleFavorite (paperId, paperInfo = {}) {
if (isFavorite(paperId)) {
return removeFavorite(paperId)
} else {
return addFavorite(paperId, paperInfo)
}
}
export function sortByFavorite (paperList) {
if (!Array.isArray(paperList) || paperList.length === 0) {
return paperList
}
const favoriteIds = getFavorites().map(item => item.id)
if (favoriteIds.length === 0) {
return paperList
}
const favoritePapers = []
const otherPapers = []
paperList.forEach(paper => {
if (favoriteIds.includes(paper.id)) {
favoritePapers.push(paper)
} else {
otherPapers.push(paper)
}
})
favoritePapers.sort((a, b) => {
const indexA = favoriteIds.indexOf(a.id)
const indexB = favoriteIds.indexOf(b.id)
return indexA - indexB
})
return [...favoritePapers, ...otherPapers]
}
export function clearFavorites () {
try {
window.localStorage.removeItem(FAVORITE_KEY)
return true
} catch (e) {
return false
}
}
@@ -1,19 +1,26 @@
<template>
<div style="margin-top: 10px" class="app-contain">
<el-tabs tab-position="left" v-model="tabId" @tab-click="subjectChange" >
<el-tab-pane :label="item.name" :key="item.id" :name="item.id" v-for="item in subjectList" style="margin-left: 20px;" >
<el-row style="float: right">
<el-radio-group v-model="queryParam.paperType" size="mini" @change="paperTypeChange" >
<el-tabs tab-position="left" v-model="tabId" @tab-click="subjectChange">
<el-tab-pane :label="item.name" :key="item.id" :name="item.id" v-for="item in subjectList" style="margin-left: 20px;">
<el-row style="float: right">
<el-radio-group v-model="queryParam.paperType" size="mini" @change="paperTypeChange">
<el-radio v-for="item in paperTypeEnum" size="mini" :key="item.key" :label="item.key">{{item.value}}</el-radio>
</el-radio-group>
</el-row>
<el-table v-loading="listLoading" :data="tableData" fit highlight-current-row style="width: 100%">
<el-table v-loading="listLoading" :data="sortedTableData" fit highlight-current-row style="width: 100%">
<el-table-column prop="id" label="序号" width="90px"/>
<el-table-column prop="name" label="名称" />
<el-table-column prop="name" label="名称"/>
<el-table-column label="收藏" width="60px" align="center">
<template slot-scope="{row}">
<el-button type="text" size="small" @click="handleToggleFavorite(row)" :class="{'is-favorite': isPaperFavorite(row.id)}">
<svg-icon :icon-class="isPaperFavorite(row.id) ? 'star' : 'star'" :class="{'favorite-icon': true, 'active': isPaperFavorite(row.id)}"/>
</el-button>
</template>
</el-table-column>
<el-table-column align="right">
<template slot-scope="{row}">
<router-link target="_blank" :to="{path:'/do',query:{id:row.id}}">
<el-button type="text" size="small">开始答题</el-button>
<el-button type="text" size="small">开始答题</el-button>
</router-link>
</template>
</el-table-column>
@@ -31,6 +38,7 @@ import { mapState } from 'vuex'
import Pagination from '@/components/Pagination'
import examPaperApi from '@/api/examPaper'
import subjectApi from '@/api/subject'
import { isFavorite, toggleFavorite, sortByFavorite } from '@/utils/favorite'
export default {
components: { Pagination },
@@ -49,6 +57,14 @@ export default {
total: 0
}
},
computed: {
...mapState('enumItem', {
paperTypeEnum: state => state.exam.examPaper.paperTypeEnum
}),
sortedTableData () {
return sortByFavorite(this.tableData)
}
},
created () {
this.initSubject()
},
@@ -79,12 +95,48 @@ export default {
subjectChange (tab, event) {
this.queryParam.subjectId = Number(this.tabId)
this.search()
},
isPaperFavorite (paperId) {
return isFavorite(paperId)
},
handleToggleFavorite (row) {
const result = toggleFavorite(row.id, {
name: row.name,
subjectId: row.subjectId
})
if (result) {
this.$message({
message: this.isPaperFavorite(row.id) ? '收藏成功' : '已取消收藏',
type: 'success',
duration: 1500
})
} else {
this.$message.error('操作失败,请稍后重试')
}
}
},
computed: {
...mapState('enumItem', {
paperTypeEnum: state => state.exam.examPaper.paperTypeEnum
})
}
}
</script>
<style scoped>
.favorite-icon {
width: 18px;
height: 18px;
fill: #c0c4cc;
transition: all 0.3s;
cursor: pointer;
}
.favorite-icon:hover {
fill: #409eff;
transform: scale(1.2);
}
.favorite-icon.active {
fill: #f7ba2a;
}
.is-favorite .favorite-icon {
fill: #f7ba2a;
}
</style>