Browse Source

feat: 新增用户中心页面并优化章节阅读界面

- 新增用户中心的礼物盒、任务中心和消息页面
- 优化章节阅读界面布局和交互
- 添加页面切换动画和未读消息功能
master
前端-胡立永 2 months ago
parent
commit
ec4b6688c2
10 changed files with 1712 additions and 315 deletions
  1. +5
    -1
      src/App.vue
  2. +24
    -1
      src/layout/index.vue
  3. +45
    -4
      src/layout/layout/Header.vue
  4. +18
    -0
      src/router/index.js
  5. +25
    -3
      src/store/index.js
  6. +320
    -305
      src/views/book/chapter.vue
  7. +3
    -1
      src/views/book/index.vue
  8. +365
    -0
      src/views/user/GiftBox.vue
  9. +493
    -0
      src/views/user/Messages.vue
  10. +414
    -0
      src/views/user/TaskCenter.vue

+ 5
- 1
src/App.vue View File

@ -1,5 +1,5 @@
<template>
<div id="app">
<div id="app" :style="{ backgroundColor: route.path.includes('chapter') ? '#e9e4d8' : '#f5f5f5' }">
<AuthProvider>
<AuthorApplicationProvider>
<router-view />
@ -54,6 +54,10 @@ export default {
routerEvents.triggerLogin = null;
}
});
return {
route
}
}
};
</script>


+ 24
- 1
src/layout/index.vue View File

@ -6,7 +6,9 @@
<!-- 主内容区 -->
<main class="layout-main">
<router-view v-slot="{ Component }">
<component :is="Component" />
<transition name="fade">
<component :is="Component" />
</transition>
</router-view>
</main>
@ -51,4 +53,25 @@ export default defineComponent({
margin: 0 auto;
width: 100%;
}
//
.slide-fade-enter-active {
transition: all 0.15s ease-out;
}
.slide-fade-leave-active {
transition: all 0.1s ease-in;
}
.slide-fade-enter-from {
transform: translateX(10px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(-10px);
opacity: 0;
}
</style>

+ 45
- 4
src/layout/layout/Header.vue View File

@ -41,8 +41,8 @@
</div>
<div class="user-info">
<div class="notification">
<el-badge :value="3" class="notification-badge">
<div class="notification" @click="goToMessages">
<el-badge :value="unreadMessageCount" class="notification-badge">
<el-icon><Bell /></el-icon>
</el-badge>
</div>
@ -69,12 +69,12 @@
</div>
</div>
<el-dropdown-item class="menu-item">
<el-dropdown-item class="menu-item" @click="goToGiftBox">
<el-icon><GiftBox /></el-icon>
<span>礼物盒</span>
</el-dropdown-item>
<el-dropdown-item class="menu-item">
<el-dropdown-item class="menu-item" @click="goToTaskCenter">
<el-icon><List /></el-icon>
<span>任务中心</span>
</el-dropdown-item>
@ -151,6 +151,7 @@ export default {
const userName = computed(() => store.user?.name || '神鹿日光');
const userAvatar = computed(() => store.user?.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png');
const beanCount = computed(() => store.user?.beanCount || '9999');
const unreadMessageCount = computed(() => store.unreadMessageCount || 0);
//
const activeMenu = computed(() => {
@ -215,6 +216,30 @@ export default {
authorApplicationContext.openApplicationModal();
};
//
const goToGiftBox = () => {
//
if (!isLoggedIn.value) {
authContext.openLogin(() => {
router.push('/gift-box');
});
return;
}
router.push('/gift-box');
};
//
const goToTaskCenter = () => {
//
if (!isLoggedIn.value) {
authContext.openLogin(() => {
router.push('/task-center');
});
return;
}
router.push('/task-center');
};
//
const goToAuthorCenter = () => {
//
@ -247,6 +272,18 @@ export default {
}
};
//
const goToMessages = () => {
//
if (!isLoggedIn.value) {
authContext.openLogin(() => {
router.push('/messages');
});
return;
}
router.push('/messages');
};
return {
isLoggedIn,
userName,
@ -258,11 +295,15 @@ export default {
goToLogin,
goToUserCenter,
goToBookshelf,
goToGiftBox,
goToTaskCenter,
goToSettings,
logout,
applyForAuthor,
goToAuthorCenter,
router,
goToMessages,
unreadMessageCount,
};
}
};


+ 18
- 0
src/router/index.js View File

@ -49,6 +49,24 @@ const routes = [
component: () => import('../views/home/Bookshelf.vue'),
meta: { requiresAuth: true }
},
{
path: 'task-center',
name: 'TaskCenter',
component: () => import('../views/user/TaskCenter.vue'),
meta: { requiresAuth: true }
},
{
path: 'gift-box',
name: 'GiftBox',
component: () => import('../views/user/GiftBox.vue'),
meta: { requiresAuth: true }
},
{
path: 'messages',
name: 'Messages',
component: () => import('../views/user/Messages.vue'),
meta: { requiresAuth: true }
},
{
path: 'author',
name: 'authorCenter',


+ 25
- 3
src/store/index.js View File

@ -6,12 +6,14 @@ export const useMainStore = defineStore('main', {
isLoggedIn: false,
isAuthor: false,
token: null,
bookshelf: []
bookshelf: [],
unreadMessages: 3 // 添加未读消息数量
}),
getters: {
isAuthenticated: (state) => state.isLoggedIn && state.user !== null,
bookshelfCount: (state) => state.bookshelf.length
bookshelfCount: (state) => state.bookshelf.length,
unreadMessageCount: (state) => state.unreadMessages // 添加获取未读消息数量的getter
},
actions: {
@ -27,7 +29,8 @@ export const useMainStore = defineStore('main', {
name: '用户' + loginData.phone.substring(loginData.phone.length - 4),
phone: loginData.phone,
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
isAuthor: false
isAuthor: false,
beanCount: 9999
};
this.user = userData;
@ -174,6 +177,25 @@ export const useMainStore = defineStore('main', {
// 更新本地存储
localStorage.setItem('bookshelf', JSON.stringify(this.bookshelf));
}
},
// 更新用户豆豆余额
updateUserBeans(newBeanCount) {
if (this.user) {
this.user.beanCount = newBeanCount;
// 更新本地存储
localStorage.setItem('user', JSON.stringify(this.user));
}
},
// 清空未读消息数量
clearUnreadMessages() {
this.unreadMessages = 0;
},
// 设置未读消息数量
setUnreadMessages(count) {
this.unreadMessages = count;
}
}
});

+ 320
- 305
src/views/book/chapter.vue View File

@ -1,226 +1,254 @@
<template>
<div class="chapter-container">
<div class="chapter-header">
<div class="header-content">
<div class="back" @click="goBack">
<el-icon><ArrowLeft /></el-icon>
<span>返回目录</span>
<div class="chapter-container">
<div class="chapter-content">
<div class="chapter-title-container">
<el-icon size="20px">
<ArrowLeft />
</el-icon>
<h1 class="chapter-title">{{ chapter.title }}</h1>
</div>
<div class="chapter-text">
<p v-for="(paragraph, index) in chapter.content" :key="index" class="paragraph">
{{ paragraph }}
</p>
</div>
</div>
<div class="title">{{ chapter.title }}</div>
<div class="book-title">{{ bookTitle }}</div>
</div>
</div>
<div class="chapter-content">
<h1 class="chapter-title">{{ chapter.title }}</h1>
<div class="content-navigation">
<el-button v-if="prevChapterId" @click="goToChapter(prevChapterId)" plain>上一章</el-button>
<el-button @click="goToBookDetail">目录</el-button>
<el-button v-if="nextChapterId" @click="goToChapter(nextChapterId)" type="primary">下一章</el-button>
</div>
<div class="chapter-text">
<p v-for="(paragraph, index) in chapter.content" :key="index" class="paragraph">
{{ paragraph }}
</p>
</div>
<div class="content-navigation bottom">
<el-button v-if="prevChapterId" @click="goToChapter(prevChapterId)" plain>上一章</el-button>
<el-button @click="goToBookDetail">目录</el-button>
<el-button v-if="nextChapterId" @click="goToChapter(nextChapterId)" type="primary">下一章</el-button>
</div>
</div>
<div class="reading-settings">
<div class="settings-buttons">
<el-button @click="toggleFontSize(2)" circle plain>
<el-icon><ZoomIn /></el-icon>
</el-button>
<el-button @click="toggleFontSize(-2)" circle plain>
<el-icon><ZoomOut /></el-icon>
</el-button>
<el-button @click="toggleTheme" circle plain>
<el-icon><MoonNight /></el-icon>
</el-button>
</div>
<div class="chapter-menu">
<div class="chapter-menu-item">
<el-icon size="20px">
<ArrowUp />
</el-icon>
<span>上一章</span>
</div>
<div class="chapter-menu-item">
<el-icon size="20px">
<ArrowDown />
</el-icon>
<span>下一章</span>
</div>
<div class="chapter-menu-item">
<el-icon size="20px">
<Reading />
</el-icon>
<span>加书架</span>
</div>
<div class="chapter-menu-item">
<el-icon size="20px">
<ArrowRight />
</el-icon>
<span>目录</span>
</div>
<div class="chapter-menu-item"
@click="toggleTheme">
<el-icon size="20px">
<MoonNight />
</el-icon>
<span>夜间</span>
</div>
</div>
<!-- <div class="reading-settings">
<div class="settings-buttons">
<el-button @click="toggleFontSize(2)" circle plain>
<el-icon>
<ZoomIn />
</el-icon>
</el-button>
<el-button @click="toggleFontSize(-2)" circle plain>
<el-icon>
<ZoomOut />
</el-icon>
</el-button>
<el-button @click="toggleTheme" circle plain>
<el-icon>
<MoonNight />
</el-icon>
</el-button>
</div>
</div> -->
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ArrowLeft, ZoomIn, ZoomOut, MoonNight } from '@element-plus/icons-vue';
import { ArrowLeft, ZoomIn, ZoomOut, MoonNight, ArrowUp, ArrowDown, Reading } from '@element-plus/icons-vue';
export default {
name: 'ChapterDetail',
components: {
ArrowLeft,
ZoomIn,
ZoomOut,
MoonNight
},
setup() {
const route = useRoute();
const router = useRouter();
const bookId = route.params.id;
const chapterId = route.params.chapterId;
const bookTitle = ref('重生之财源滚滚');
const fontSize = ref(18);
const isDarkMode = ref(false);
//
const chapter = ref({
id: chapterId,
title: '第' + chapterId + '章 ' + (chapterId === '1' ? '重回2004' : '精彩内容'),
content: [
'贺季宁站在天台上,俯瞰着城市的点点灯光,感到一阵恍惚。',
'他曾经有多么意气风发,现在就有多么落魄。',
'三年前,他是A市土豪公司的金牌经理,工资高,福利好,女友漂亮,车位靓,最重要的是老板信任他。',
'但是贺季宁的对手们不服,老板娘不喜欢他,不怀好意的对手们联合一起,终于把他拉下台了。',
'他失去工作,更可悲的是,他连公司配他的贷款的房子都搭进去了,欠了几百万的债,女友也离他而去。',
'这三年,他一直试图东山再起,却处处碰壁,如今债主们逼得紧,他已经走投无路。',
'"如果老天再给我一次机会,我一定要好好做人,赚很多钱,让那帮人看着我过得好,他们就会比我难受!"',
'贺季宁苦笑着喃喃自语,闭上了眼睛。',
'他感觉自己似乎在坠落,又似乎在飘浮,时间不知过了多久...',
'当他再次睁开眼睛,发现自己躺在一张熟悉又陌生的床上。',
'这是他十几年前住过的出租屋!',
'贺季宁一个鲤鱼打挺坐了起来,环顾四周,破旧的家具,泛黄的墙壁,一切都像是回到了过去。',
'他猛地抓起床头的手机——诺基亚!',
'日期显示:2004年3月15日。',
'"我...重生了?"贺季宁不敢相信自己的眼睛,他竟然回到了2004年,回到了一切开始之前!',
'一切都可以重来!所有的错误都可以避免!他知道未来的风口,知道哪些投资会成功,知道哪些陷阱要避开。',
'贺季宁握紧了拳头,眼中闪烁着坚定的光芒。',
'"这一次,我一定要扭转命运!"'
]
});
// ID
const prevChapterId = computed(() => {
const current = parseInt(chapterId);
return current > 1 ? (current - 1).toString() : null;
});
const nextChapterId = computed(() => {
const current = parseInt(chapterId);
return current < 50 ? (current + 1).toString() : null;
});
//
const goToBookDetail = () => {
router.push(`/book/${bookId}`);
};
//
const goBack = () => {
router.back();
};
//
const goToChapter = (targetChapterId) => {
router.push(`/book/${bookId}/chapter/${targetChapterId}`);
};
//
const toggleFontSize = (change) => {
const newSize = fontSize.value + change;
if (newSize >= 14 && newSize <= 24) {
fontSize.value = newSize;
document.documentElement.style.setProperty('--reading-font-size', `${fontSize.value}px`);
}
};
//
const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value;
if (isDarkMode.value) {
document.documentElement.classList.add('dark-theme');
} else {
document.documentElement.classList.remove('dark-theme');
}
};
onMounted(() => {
//
document.documentElement.style.setProperty('--reading-font-size', `${fontSize.value}px`);
//
window.scrollTo(0, 0);
// API
console.log(`加载章节内容,书籍ID: ${bookId},章节ID: ${chapterId}`);
});
return {
bookTitle,
chapter,
prevChapterId,
nextChapterId,
goToBookDetail,
goBack,
goToChapter,
toggleFontSize,
toggleTheme
};
}
name: 'ChapterDetail',
components: {
ArrowLeft,
ZoomIn,
ZoomOut,
MoonNight,
ArrowUp,
ArrowDown,
Reading
},
setup() {
const route = useRoute();
const router = useRouter();
const bookId = route.params.id;
const chapterId = route.params.chapterId;
const bookTitle = ref('重生之财源滚滚');
const fontSize = ref(18);
const isDarkMode = ref(false);
//
const chapter = ref({
id: chapterId,
title: '第' + chapterId + '章 ' + (chapterId === '1' ? '重回2004' : '精彩内容'),
content: [
'贺季宁站在天台上,俯瞰着城市的点点灯光,感到一阵恍惚。',
'他曾经有多么意气风发,现在就有多么落魄。',
'三年前,他是A市土豪公司的金牌经理,工资高,福利好,女友漂亮,车位靓,最重要的是老板信任他。三年前,他是A市土豪公司的金牌经理,工资高,福利好,女友漂亮,车位靓,最重要的是老板信任他。',
'但是贺季宁的对手们不服,老板娘不喜欢他,不怀好意的对手们联合一起,终于把他拉下台了。',
'他失去工作,更可悲的是,他连公司配他的贷款的房子都搭进去了,欠了几百万的债,女友也离他而去。',
'这三年,他一直试图东山再起,却处处碰壁,如今债主们逼得紧,他已经走投无路。',
'"如果老天再给我一次机会,我一定要好好做人,赚很多钱,让那帮人看着我过得好,他们就会比我难受!"',
'贺季宁苦笑着喃喃自语,闭上了眼睛。',
'他感觉自己似乎在坠落,又似乎在飘浮,时间不知过了多久...',
'当他再次睁开眼睛,发现自己躺在一张熟悉又陌生的床上。',
'这是他十几年前住过的出租屋!',
'贺季宁一个鲤鱼打挺坐了起来,环顾四周,破旧的家具,泛黄的墙壁,一切都像是回到了过去。',
'他猛地抓起床头的手机——诺基亚!',
'日期显示:2004年3月15日。',
'"我...重生了?"贺季宁不敢相信自己的眼睛,他竟然回到了2004年,回到了一切开始之前!',
'一切都可以重来!所有的错误都可以避免!他知道未来的风口,知道哪些投资会成功,知道哪些陷阱要避开。',
'贺季宁握紧了拳头,眼中闪烁着坚定的光芒。',
'"这一次,我一定要扭转命运!"'
]
});
// ID
const prevChapterId = computed(() => {
const current = parseInt(chapterId);
return current > 1 ? (current - 1).toString() : null;
});
const nextChapterId = computed(() => {
const current = parseInt(chapterId);
return current < 50 ? (current + 1).toString() : null;
});
//
const goToBookDetail = () => {
router.push(`/book/${bookId}`);
};
//
const goBack = () => {
router.back();
};
//
const goToChapter = (targetChapterId) => {
router.push(`/book/${bookId}/chapter/${targetChapterId}`);
};
//
const toggleFontSize = (change) => {
const newSize = fontSize.value + change;
if (newSize >= 14 && newSize <= 24) {
fontSize.value = newSize;
document.documentElement.style.setProperty('--reading-font-size', `${fontSize.value}px`);
}
};
//
const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value;
if (isDarkMode.value) {
document.documentElement.classList.add('dark-theme');
} else {
document.documentElement.classList.remove('dark-theme');
}
};
onMounted(() => {
//
document.documentElement.style.setProperty('--reading-font-size', `${fontSize.value}px`);
//
window.scrollTo(0, 0);
// API
console.log(`加载章节内容,书籍ID: ${bookId},章节ID: ${chapterId}`);
});
return {
bookTitle,
chapter,
prevChapterId,
nextChapterId,
goToBookDetail,
goBack,
goToChapter,
toggleFontSize,
toggleTheme
};
}
};
</script>
<style lang="scss">
//
:root {
--reading-font-size: 18px;
--reading-background: #f8f8f8;
--reading-text-color: #333;
--reading-border-color: #eee;
--reading-font-size: 18px;
--reading-background: #f8f8f8;
--reading-text-color: #333;
--reading-border-color: #eee;
}
.dark-theme {
--reading-background: #252525;
--reading-text-color: #ccc;
--reading-border-color: #333;
.chapter-container {
background-color: var(--reading-background) !important;
.chapter-header {
background-color: #202020 !important;
color: #ddd !important;
border-bottom-color: #333 !important;
}
.chapter-content {
background-color: var(--reading-background) !important;
color: var(--reading-text-color) !important;
.chapter-title {
color: #eee !important;
}
.paragraph {
color: var(--reading-text-color) !important;
}
}
.el-button {
background-color: #333 !important;
border-color: #555 !important;
color: #ddd !important;
&.is-plain {
background: transparent !important;
}
&.el-button--primary {
background-color: var(--el-color-primary) !important;
border-color: var(--el-color-primary) !important;
color: #fff !important;
}
--reading-background: #252525;
--reading-text-color: #ccc;
--reading-border-color: #333;
.chapter-container {
background-color: var(--reading-background) !important;
.chapter-header {
background-color: #202020 !important;
color: #ddd !important;
border-bottom-color: #333 !important;
}
.chapter-content {
background-color: var(--reading-background) !important;
color: var(--reading-text-color) !important;
.chapter-title {
color: #eee !important;
}
.paragraph {
color: var(--reading-text-color) !important;
}
}
.el-button {
background-color: #333 !important;
border-color: #555 !important;
color: #ddd !important;
&.is-plain {
background: transparent !important;
}
&.el-button--primary {
background-color: var(--el-color-primary) !important;
border-color: var(--el-color-primary) !important;
color: #fff !important;
}
}
}
}
}
</style>
@ -228,125 +256,112 @@ export default {
@use '@/assets/styles/variables.scss' as vars;
.chapter-container {
min-height: 100vh;
background-color: var(--reading-background);
display: flex;
flex-direction: column;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.chapter-header {
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid var(--reading-border-color);
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1000px;
.chapter-content {
flex: 1;
max-width: 1100px;
margin: 0 auto;
padding: 15px 20px;
.back {
display: flex;
align-items: center;
cursor: pointer;
color: vars.$primary-color;
.el-icon {
margin-right: 5px;
}
}
.title {
font-weight: bold;
font-size: 16px;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 30px 20px 50px;
background-color: #F3EFE6;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
.chapter-title-container {
display: flex;
align-items: center;
margin-bottom: 30px;
padding-bottom: 10px;
border-bottom: 1px solid #e9e4d8;
cursor: pointer;
.chapter-title {
display: flex;
align-items: center;
text-align: left;
font-size: 24px;
color: #333;
margin-left: 10px;
}
}
.book-title {
font-size: 14px;
color: #666;
.chapter-text {
line-height: 1.8;
.paragraph {
margin-bottom: 20px;
font-size: var(--reading-font-size);
color: var(--reading-text-color);
text-indent: 2em;
}
}
}
}
.chapter-content {
flex: 1;
max-width: 800px;
margin: 0 auto;
padding: 30px 20px 50px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
.chapter-title {
text-align: center;
font-size: 24px;
margin-bottom: 30px;
color: #333;
}
.content-navigation {
.chapter-menu {
position: fixed;
bottom: 20%;
left: 50%;
transform: translateX(550px);
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
&.bottom {
margin-top: 50px;
margin-bottom: 0;
flex-direction: column;
align-items: center;
margin-top: 20px;
padding: 0 20px;
gap: 10px;
.chapter-menu-item {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 10px;
background-color: #F3EFE6;
cursor: pointer;
height: 80px;
width: 80px;
font-size: 14px;
gap: 5px;
}
}
.chapter-text {
line-height: 1.8;
.paragraph {
margin-bottom: 20px;
font-size: var(--reading-font-size);
color: var(--reading-text-color);
text-indent: 2em;
}
}
}
.reading-settings {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 10;
.settings-buttons {
display: flex;
flex-direction: column;
gap: 10px;
.el-button {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
position: fixed;
bottom: 30px;
right: 30px;
z-index: 10;
.settings-buttons {
display: flex;
flex-direction: column;
gap: 10px;
.el-button {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
}
}
}
//
@media screen and (max-width: vars.$md) {
.chapter-content {
padding: 20px 15px 40px;
}
.chapter-header {
.header-content {
padding: 12px 15px;
.title {
max-width: 200px;
}
.chapter-content {
padding: 20px 15px 40px;
}
.chapter-header {
.header-content {
padding: 12px 15px;
.title {
max-width: 200px;
}
}
}
}
}
</style>
</style>

+ 3
- 1
src/views/book/index.vue View File

@ -48,7 +48,9 @@
>{{ isInShelf ? '已加入书架' : '加入书架' }}</el-button>
</div>
<div class="action-btn-group">
<el-button class="read-btn" style="background:#0A2463;color:#fff;border:none;">点击阅读</el-button>
<el-button class="read-btn"
@click="goToChapter(1)"
style="background:#0A2463;color:#fff;border:none;">点击阅读</el-button>
</div>
</div>
</div>


+ 365
- 0
src/views/user/GiftBox.vue View File

@ -0,0 +1,365 @@
<template>
<div class="gift-box">
<!-- 页面标题 -->
<div class="header">
<h1 class="title">
<el-icon class="title-icon"><Star /></el-icon>
我的礼物盒
</h1>
</div>
<!-- 礼物网格 -->
<div class="gifts-grid">
<div
v-for="gift in gifts"
:key="gift.id"
class="gift-card"
:class="{ 'special': gift.isSpecial }"
>
<!-- 特价标签 -->
<div v-if="gift.isSpecial" class="special-tag"></div>
<!-- 礼物图标 -->
<div class="gift-icon">
<span class="emoji">{{ gift.emoji }}</span>
</div>
<!-- 礼物名称 -->
<div class="gift-name">{{ gift.name }}</div>
<!-- 价格 -->
<div class="gift-price">{{ gift.price }} 豆豆</div>
<!-- 购买按钮 -->
<el-button
type="primary"
size="small"
@click="buyGift(gift)"
:disabled="gift.price > userBeans"
>
购买
</el-button>
</div>
</div>
<!-- 加载更多 -->
<div class="load-more">
<div class="divider">
<el-icon class="divider-icon"><MoreFilled /></el-icon>
</div>
<p class="no-more-text">没有更多啦</p>
</div>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useMainStore } from '@/store';
import { MoreFilled, Star } from '@element-plus/icons-vue';
export default {
name: 'GiftBox',
components: {
MoreFilled,
Star
},
setup() {
const store = useMainStore();
//
const userBeans = computed(() => store.user?.beanCount || 9999);
//
const gifts = reactive([
//
{ id: 1, name: '小星星', price: 5, emoji: '💖', isSpecial: true },
{ id: 2, name: '爱你哦', price: 10, emoji: '💋', isSpecial: true },
{ id: 3, name: '加油鸭', price: 15, emoji: '🍊', isSpecial: true },
{ id: 4, name: '蓝色烟火', price: 20, emoji: '🎆', isSpecial: true },
{ id: 5, name: '西瓜', price: 10, emoji: '🍉', isSpecial: true },
{ id: 6, name: '冰棍', price: 8, emoji: '🍭', isSpecial: true },
//
{ id: 7, name: '沙滩椅', price: 15, emoji: '🏖️', isSpecial: true },
{ id: 8, name: '菠萝汽水', price: 30, emoji: '🍍', isSpecial: true },
{ id: 9, name: '椰子水', price: 40, emoji: '🥥' },
{ id: 10, name: '汽水', price: 20, emoji: '🥤' },
{ id: 11, name: '咖啡', price: 60, emoji: '☕', isSpecial: true },
{ id: 12, name: '早餐套餐', price: 88, emoji: '🍳', isSpecial: true },
//
{ id: 13, name: '花花', price: 188, emoji: '💜', isSpecial: true },
{ id: 14, name: '玫瑰', price: 199, emoji: '🌹', isSpecial: true },
{ id: 15, name: '玫瑰花', price: 399, emoji: '🌹', isSpecial: true },
{ id: 16, name: '玫瑰礼盒', price: 599, emoji: '🎁' },
{ id: 17, name: '礼花', price: 589, emoji: '🎊' },
{ id: 18, name: '灿烂辉煌', price: 699, emoji: '🎇', isSpecial: true },
//
{ id: 19, name: '紫色星空', price: 766, emoji: '🌌', isSpecial: true },
{ id: 20, name: '跑车', price: 799, emoji: '🏎️' },
{ id: 21, name: '直升机', price: 899, emoji: '🚁' },
{ id: 22, name: '游艇', price: 966, emoji: '🛥️' },
{ id: 23, name: '黄金直升机', price: 999, emoji: '🚁' }
]);
//
const buyGift = async (gift) => {
if (gift.price > userBeans.value) {
ElMessage.error('豆豆余额不足,请先充值!');
return;
}
try {
await ElMessageBox.confirm(
`确定要购买 ${gift.name} 吗?将花费 ${gift.price} 豆豆`,
'确认购买',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
// API
await new Promise(resolve => setTimeout(resolve, 500));
//
if (store.updateUserBeans) {
store.updateUserBeans(userBeans.value - gift.price);
}
ElMessage.success(`成功购买 ${gift.name}`);
} catch {
//
}
};
onMounted(() => {
console.log('礼物盒页面已加载');
});
return {
gifts,
userBeans,
buyGift
};
}
};
</script>
<style lang="scss" scoped>
.gift-box {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
.header {
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.title {
display: flex;
align-items: center;
font-size: 24px;
font-weight: 600;
color: #0A2463;
margin: 0;
.title-icon {
margin-right: 8px;
font-size: 28px;
}
}
}
.gifts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
margin-bottom: 40px;
.gift-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 12px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
&.special {
background: linear-gradient(135deg, #fff9e6 0%, #fff 100%);
border-color: #ffa726;
}
.special-tag {
position: absolute;
top: -1px;
right: -1px;
background: #ff6b35;
color: white;
font-size: 12px;
font-weight: bold;
width: 24px;
height: 24px;
border-radius: 50% 0 12px 0;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.gift-icon {
font-size: 48px;
margin-bottom: 12px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
.emoji {
font-size: 48px;
line-height: 1;
}
}
.gift-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.gift-price {
font-size: 14px;
color: #ffa726;
font-weight: 600;
margin-bottom: 12px;
}
.el-button {
width: 100%;
background-color: #0A2463;
border-color: #0A2463;
&:hover {
background-color: #083354;
border-color: #083354;
}
&:disabled {
background-color: #c0c4cc;
border-color: #c0c4cc;
color: #a8abb2;
}
}
}
}
.load-more {
text-align: center;
padding: 20px 0;
.divider {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, #ddd, transparent);
margin: 0 20px;
}
.divider-icon {
color: #ffa726;
font-size: 20px;
transform: rotate(90deg);
}
}
.no-more-text {
color: #999;
font-size: 14px;
margin: 0;
}
}
}
//
@media (max-width: 768px) {
.gift-box {
padding: 16px;
.gifts-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 16px;
.gift-card {
padding: 16px;
.gift-icon {
font-size: 36px;
height: 50px;
.emoji {
font-size: 36px;
}
}
.gift-name {
font-size: 14px;
}
.gift-price {
font-size: 13px;
}
}
}
}
}
@media (max-width: 480px) {
.gift-box {
.gifts-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
.gift-card {
padding: 12px;
.gift-icon {
font-size: 32px;
height: 40px;
.emoji {
font-size: 32px;
}
}
.gift-name {
font-size: 13px;
}
.gift-price {
font-size: 12px;
}
}
}
}
}
</style>

+ 493
- 0
src/views/user/Messages.vue View File

@ -0,0 +1,493 @@
<template>
<div class="messages">
<!-- 页面标题和一键清空 -->
<div class="header">
<h1 class="title">
<el-icon class="title-icon"><Message /></el-icon>
我的消息
</h1>
<el-button
type="primary"
size="small"
@click="clearAllMessages"
:disabled="messages.length === 0"
>
一键清空
</el-button>
</div>
<!-- 消息列表 -->
<div class="messages-list">
<div
v-for="message in messages"
:key="message.id"
class="message-item"
>
<!-- 用户信息 -->
<div class="user-info">
<el-avatar :size="40" :src="message.avatar" />
<div class="user-details">
<div class="user-name">{{ message.username }}</div>
<div class="source-info">
来自{{ message.bookTitle }}
<span class="time">{{ message.time }}</span>
</div>
</div>
</div>
<!-- 评论内容 -->
<div class="comment-content">
{{ message.content }}
</div>
<!-- 回复按钮 -->
<div class="action-buttons">
<el-button
type="text"
size="small"
@click="toggleReply(message.id)"
class="reply-btn"
>
回复
</el-button>
</div>
<!-- 回复框 -->
<div v-if="message.showReply" class="reply-section">
<el-input
v-model="message.replyContent"
type="textarea"
:rows="3"
placeholder="请输入回复内容..."
class="reply-input"
/>
<div class="reply-actions">
<el-button
size="small"
@click="cancelReply(message.id)"
>
取消
</el-button>
<el-button
type="primary"
size="small"
@click="submitReply(message.id)"
:disabled="!message.replyContent?.trim()"
>
发送
</el-button>
</div>
</div>
<!-- 已有回复 -->
<div v-if="message.reply" class="existing-reply">
{{ message.reply }}
</div>
</div>
<!-- 空状态 -->
<div v-if="messages.length === 0" class="empty-state">
<el-icon class="empty-icon"><Message /></el-icon>
<p>暂无消息</p>
</div>
</div>
<!-- 分页器 -->
<div v-if="total > pageSize" class="pagination">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useMainStore } from '@/store';
import { Message } from '@element-plus/icons-vue';
export default {
name: 'Messages',
components: {
Message
},
setup() {
const store = useMainStore();
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
//
const messages = reactive([
{
id: 1,
username: '吴宽宽',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
bookTitle: '重生之财源滚滚',
time: '2025.03.18',
content: '故事十八期人啊',
showReply: false,
replyContent: '',
reply: null
},
{
id: 2,
username: '孙娜茹',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
bookTitle: '大宋好厨夫',
time: '2025.03.18',
content: '可爱回现我等肴丽厨好像这禅。瞧的选善高大声',
showReply: false,
replyContent: '',
reply: '这样好像不正常'
},
{
id: 3,
username: '李世海',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
bookTitle: '大宋好厨夫',
time: '2025.03.18',
content: '一获乘蕾又舞一段',
showReply: false,
replyContent: '',
reply: '这样好像不正常'
},
{
id: 4,
username: '钱萌萌',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
bookTitle: '重生之财源滚滚',
time: '2025.03.18',
content: '就是开快乐开(表情)',
showReply: false,
replyContent: '',
reply: null
},
{
id: 5,
username: 'ty_zhen',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
bookTitle: '大宋好厨夫',
time: '2025.03.18',
content: '说得主角越来越明白不对劲。他知道受罗',
showReply: false,
replyContent: '',
reply: '这样好像不正常'
},
{
id: 6,
username: 'mengmeng',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
bookTitle: '大宋好厨夫',
time: '2025.03.18',
content: '虽然回忆的一次故事,看过的分别还是。看在提供的难过,因为明代来实受应的沧桑感蒙受兴了',
showReply: false,
replyContent: '',
reply: '这样好像不正常'
}
]);
total.value = messages.length;
//
const toggleReply = (messageId) => {
const message = messages.find(m => m.id === messageId);
if (message) {
//
messages.forEach(m => {
if (m.id !== messageId) {
m.showReply = false;
m.replyContent = '';
}
});
message.showReply = !message.showReply;
if (!message.showReply) {
message.replyContent = '';
}
}
};
//
const cancelReply = (messageId) => {
const message = messages.find(m => m.id === messageId);
if (message) {
message.showReply = false;
message.replyContent = '';
}
};
//
const submitReply = async (messageId) => {
const message = messages.find(m => m.id === messageId);
if (message && message.replyContent?.trim()) {
try {
// API
await new Promise(resolve => setTimeout(resolve, 500));
message.reply = message.replyContent.trim();
message.showReply = false;
message.replyContent = '';
ElMessage.success('回复成功!');
} catch (error) {
ElMessage.error('回复失败,请重试');
}
}
};
//
const clearAllMessages = async () => {
try {
await ElMessageBox.confirm(
'确定要清空所有消息吗?此操作不可恢复',
'确认清空',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
messages.splice(0);
total.value = 0;
currentPage.value = 1;
ElMessage.success('消息已全部清空');
} catch {
//
}
};
//
const handlePageChange = (page) => {
currentPage.value = page;
//
};
onMounted(() => {
console.log('消息页面已加载');
//
store.clearUnreadMessages();
});
return {
messages,
currentPage,
pageSize,
total,
toggleReply,
cancelReply,
submitReply,
clearAllMessages,
handlePageChange
};
}
};
</script>
<style lang="scss" scoped>
.messages {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.title {
display: flex;
align-items: center;
font-size: 24px;
font-weight: 600;
color: #0A2463;
margin: 0;
.title-icon {
margin-right: 8px;
font-size: 28px;
}
}
.el-button {
background-color: #0A2463;
border-color: #0A2463;
&:hover {
background-color: #083354;
border-color: #083354;
}
}
}
.messages-list {
.message-item {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 12px;
.user-details {
margin-left: 12px;
.user-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.source-info {
font-size: 14px;
color: #909399;
.time {
margin-left: 8px;
}
}
}
}
.comment-content {
color: #606266;
line-height: 1.6;
margin-bottom: 16px;
font-size: 15px;
}
.action-buttons {
text-align: right;
.reply-btn {
color: #0A2463;
font-size: 14px;
&:hover {
color: #083354;
}
}
}
.reply-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
.reply-input {
margin-bottom: 12px;
}
.reply-actions {
text-align: right;
.el-button + .el-button {
margin-left: 8px;
}
.el-button--primary {
background-color: #0A2463;
border-color: #0A2463;
&:hover {
background-color: #083354;
border-color: #083354;
}
}
}
}
.existing-reply {
margin-top: 16px;
padding: 12px;
background: #f8f9fa;
border-left: 4px solid #0A2463;
color: #606266;
font-size: 14px;
border-radius: 0 4px 4px 0;
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #909399;
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
color: #dcdfe6;
}
p {
font-size: 16px;
margin: 0;
}
}
}
.pagination {
display: flex;
justify-content: center;
margin-top: 32px;
}
}
//
@media (max-width: 768px) {
.messages {
padding: 16px;
.header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.title {
font-size: 20px;
}
}
.messages-list {
.message-item {
padding: 16px;
.user-info {
.user-details {
.user-name {
font-size: 15px;
}
.source-info {
font-size: 13px;
}
}
}
.comment-content {
font-size: 14px;
}
}
}
}
}
</style>

+ 414
- 0
src/views/user/TaskCenter.vue View File

@ -0,0 +1,414 @@
<template>
<div class="task-center">
<!-- 页面标题和剩余推荐票 -->
<div class="header">
<h1 class="title">
<el-icon class="title-icon"><List /></el-icon>
任务中心
</h1>
<div class="remaining-tickets">
剩余刷新
<span class="count">{{ remainingTickets }}</span>
推荐票
</div>
</div>
<!-- 每日签到 -->
<div class="section">
<h2 class="section-title">每日签到</h2>
<div class="sign-in-cards">
<div
v-for="day in signInDays"
:key="day.day"
class="sign-card"
:class="{ 'completed': day.completed, 'current': day.current }"
>
<div class="card-header">
<span class="day">{{ day.day }}</span>
<el-icon v-if="day.completed" class="check-icon"><Check /></el-icon>
</div>
<div class="reward">
<img src="@/assets/images/推荐票.png" alt="推荐票" class="reward-icon">
<span>推荐票 +{{ day.reward }}</span>
</div>
<el-button
:type="day.completed ? 'success' : 'primary'"
size="small"
:disabled="day.completed || !day.current"
@click="signIn(day)"
>
{{ day.completed ? '已签到' : '签到' }}
</el-button>
</div>
</div>
</div>
<!-- 更多任务 -->
<div class="section">
<h2 class="section-title">更多任务</h2>
<div class="task-cards">
<div
v-for="task in moreTasks"
:key="task.id"
class="task-card"
>
<div class="task-info">
<h3 class="task-name">{{ task.name }}</h3>
<div class="task-reward">
<img
v-if="task.rewardType === 'ticket'"
src="@/assets/images/推荐票.png"
alt="推荐票"
class="reward-icon"
>
<span>{{ task.rewardText }}</span>
</div>
</div>
<el-button
type="warning"
size="small"
:disabled="task.completed"
@click="completeTask(task)"
>
{{ task.completed ? '已完成' : '去领取' }}
</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { List, Check } from '@element-plus/icons-vue';
export default {
name: 'TaskCenter',
components: {
List,
Check
},
setup() {
const remainingTickets = ref(9);
//
const signInDays = reactive([
{ day: 1, reward: 1, completed: true, current: false },
{ day: 2, reward: 1, completed: false, current: true },
{ day: 3, reward: 1, completed: false, current: false },
{ day: 4, reward: 1, completed: false, current: false },
{ day: 5, reward: 1, completed: false, current: false },
{ day: 6, reward: 1, completed: false, current: false },
{ day: 7, reward: 1, completed: false, current: false }
]);
//
const moreTasks = reactive([
{
id: 1,
name: '观看视频广告',
rewardType: 'ticket',
rewardText: '推荐票 +1',
completed: false
},
{
id: 2,
name: '每日首阅三个章节',
rewardType: 'ticket',
rewardText: '推荐票 +1',
completed: false
},
{
id: 3,
name: '每日首条评论',
rewardType: 'ticket',
rewardText: '推荐票 +1',
completed: false
},
{
id: 4,
name: '充值金额购买',
rewardType: 'bean',
rewardText: '66豆豆 +1',
completed: false
}
]);
//
const signIn = (day) => {
if (day.completed || !day.current) return;
day.completed = true;
remainingTickets.value += day.reward;
//
const nextDayIndex = signInDays.findIndex(d => d.day === day.day + 1);
if (nextDayIndex !== -1) {
signInDays[nextDayIndex].current = true;
}
day.current = false;
ElMessage.success(`签到成功!获得推荐票 +${day.reward}`);
};
//
const completeTask = (task) => {
if (task.completed) return;
task.completed = true;
if (task.rewardType === 'ticket') {
remainingTickets.value += 1;
ElMessage.success(`任务完成!获得${task.rewardText}`);
} else {
ElMessage.success(`任务完成!获得${task.rewardText}`);
}
};
onMounted(() => {
//
console.log('任务中心页面已加载');
});
return {
remainingTickets,
signInDays,
moreTasks,
signIn,
completeTask
};
}
};
</script>
<style lang="scss" scoped>
.task-center {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.title {
display: flex;
align-items: center;
font-size: 24px;
font-weight: 600;
color: #0A2463;
margin: 0;
.title-icon {
margin-right: 8px;
font-size: 28px;
}
}
.remaining-tickets {
font-size: 14px;
color: #666;
.count {
color: #0A2463;
font-weight: 600;
font-size: 16px;
}
}
}
.section {
margin-bottom: 40px;
.section-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
}
.sign-in-cards {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 16px;
max-width: 900px;
.sign-card {
background: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
text-align: center;
transition: all 0.3s ease;
position: relative;
&.completed {
background: #e8f5e8;
border-color: #67c23a;
.check-icon {
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
background: #67c23a;
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
}
&.current {
background: #fff7e6;
border-color: #ffa726;
box-shadow: 0 2px 8px rgba(255, 167, 38, 0.2);
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
position: relative;
.day {
font-size: 14px;
font-weight: 600;
color: #303133;
}
}
.reward {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
font-size: 12px;
color: #666;
.reward-icon {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
.el-button {
width: 100%;
&.el-button--success {
background-color: #67c23a;
border-color: #67c23a;
}
&.el-button--primary {
background-color: #0A2463;
border-color: #0A2463;
}
}
}
}
.task-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
.task-card {
background: #faf8f0;
border: 1px solid #f0e6cc;
border-radius: 8px;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.task-info {
flex: 1;
.task-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
}
.task-reward {
display: flex;
align-items: center;
font-size: 14px;
color: #666;
.reward-icon {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
}
.el-button {
margin-left: 16px;
min-width: 80px;
}
}
}
}
//
@media (max-width: 768px) {
.task-center {
padding: 16px;
.header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.title {
font-size: 20px;
}
}
.sign-in-cards {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
.sign-card {
padding: 12px;
}
}
.task-cards {
grid-template-columns: 1fr;
.task-card {
flex-direction: column;
align-items: flex-start;
text-align: left;
.el-button {
margin-left: 0;
margin-top: 12px;
width: 100%;
}
}
}
}
}
</style>

Loading…
Cancel
Save