hookehuyr

feat(积分): 新增积分攻略列表页面及相关功能

添加积分攻略列表页面,包含以下功能:
- 积分攻略分类展示和筛选
- 搜索功能
- 攻略详情弹窗展示
- 从积分详情页跳转到列表页
- 添加NutToast组件支持
......@@ -17,6 +17,7 @@ declare module 'vue' {
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PointsCollector: typeof import('./src/components/PointsCollector.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
......
......@@ -28,6 +28,7 @@ export default {
'pages/EditFamily/index',
'pages/AlbumList/index',
'pages/ActivitiesCover/index',
'pages/PointsList/index',
],
window: {
backgroundTextStyle: 'light',
......
......@@ -14,9 +14,9 @@
<view class="bg-white rounded-t-3xl px-4 pt-5">
<view class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">积分攻略</h3>
<view class="text-blue-500 text-sm flex items-center">
<view @tap="handleViewAll" class="text-blue-500 text-sm flex items-center">
查看全部
<Right size="16" />
<!-- <Right size="16" /> -->
</view>
</view>
<!-- Strategy cards -->
......@@ -90,6 +90,7 @@
<script setup>
import { ref, computed } from 'vue';
import Taro from '@tarojs/taro';
import AppHeader from '../../components/AppHeader.vue';
import BottomNav from '../../components/BottomNav.vue';
import { Right, My } from '@nutui/icons-vue-taro';
......@@ -140,4 +141,10 @@ const filteredPoints = computed(() => {
}
return pointsHistory.value.filter(p => p.type === activeTab.value);
});
const handleViewAll = () => {
Taro.navigateTo({
url: '/pages/PointsList/index'
})
}
</script>
......
export default {
navigationBarTitleText: '积分攻略',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5',
enablePullDownRefresh: false
}
\ No newline at end of file
.points-list-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
// 搜索区域
.search-section {
padding: 32rpx;
background-color: white;
border-bottom: 1rpx solid #f0f0f0;
}
.search-box {
position: relative;
background-color: #f8f9fa;
border-radius: 48rpx;
padding: 0 32rpx;
height: 80rpx;
display: flex;
align-items: center;
}
.search-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
background: transparent;
border: none;
outline: none;
&::placeholder {
color: #999;
}
}
// 分类区域
.category-section {
padding: 32rpx;
background-color: white;
border-bottom: 1rpx solid #f0f0f0;
}
.category-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
}
.category-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.category-item {
padding: 16rpx 32rpx;
background-color: #f8f9fa;
border-radius: 32rpx;
border: 2rpx solid transparent;
transition: all 0.3s ease;
&.active {
background-color: #e6f7ff;
border-color: #1890ff;
.category-name {
color: #1890ff;
font-weight: bold;
}
}
}
.category-name {
font-size: 26rpx;
color: #666;
transition: color 0.3s ease;
}
// 列表区域
.points-list-section {
flex: 1;
background-color: white;
}
.points-list {
width: 100%;
}
.points-items {
padding: 0 32rpx;
}
.points-item {
display: flex;
align-items: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.3s ease;
&:active {
background-color: #f8f9fa;
}
&:last-child {
border-bottom: none;
}
}
.points-item-left {
flex: 1;
margin-right: 24rpx;
}
.points-content {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.points-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
}
.points-desc {
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
.points-reward {
display: flex;
align-items: center;
}
.reward-text {
font-size: 24rpx;
color: #52c41a;
background-color: #f6ffed;
padding: 8rpx 16rpx;
border-radius: 16rpx;
border: 1rpx solid #b7eb8f;
}
.points-item-right {
display: flex;
align-items: center;
}
.arrow-text {
font-size: 32rpx;
color: #ccc;
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 32rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 24rpx;
}
// 详情弹窗
.detail-popup {
width: 100%;
height: 100%;
background-color: white;
display: flex;
flex-direction: column;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
background-color: white;
position: sticky;
top: 0;
z-index: 10;
}
.detail-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
flex: 1;
margin-right: 24rpx;
}
.close-btn1 {
padding: 16rpx 24rpx;
background-color: #f0f0f0;
border-radius: 24rpx;
transition: background-color 0.3s ease;
&:active {
background-color: #e0e0e0;
}
}
.close-text {
font-size: 26rpx;
color: #666;
}
.detail-content {
flex: 1;
}
.detail-body {
padding: 32rpx;
}
.detail-section {
margin-bottom: 48rpx;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
position: relative;
&::before {
content: '';
position: absolute;
left: -16rpx;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 24rpx;
background-color: #1890ff;
border-radius: 3rpx;
}
}
.section-content {
font-size: 28rpx;
line-height: 1.6;
color: #666;
}
.solution-content {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.solution-step {
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.step-number {
width: 48rpx;
height: 48rpx;
background-color: #1890ff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: bold;
flex-shrink: 0;
}
.step-content {
flex: 1;
font-size: 28rpx;
line-height: 1.5;
color: #666;
padding-top: 8rpx;
}
// 积分奖励区域
.reward-section {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.reward-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
border-left: 6rpx solid #52c41a;
}
.reward-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.reward-value {
font-size: 32rpx;
color: #52c41a;
font-weight: bold;
&.bonus {
color: #fa8c16;
}
}
// 注意事项
.notes-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.note-item {
padding: 16rpx 20rpx;
background-color: #fff7e6;
border-radius: 8rpx;
border-left: 4rpx solid #ffd591;
}
.note-text {
font-size: 26rpx;
line-height: 1.5;
color: #d46b08;
}
// 响应式适配
@media (max-width: 750rpx) {
.category-grid {
gap: 12rpx;
}
.category-item {
padding: 12rpx 24rpx;
}
.category-name {
font-size: 24rpx;
}
.points-title {
font-size: 30rpx;
}
.points-desc {
font-size: 24rpx;
}
}
\ No newline at end of file
<!--
* @Description: 积分攻略列表页面
-->
<template>
<view class="points-list-page">
<!-- 搜索框 -->
<view class="search-section">
<view class="search-box">
<input
v-model="searchKeyword"
placeholder="搜索积分攻略"
class="search-input"
@input="handleSearch"
/>
</view>
</view>
<!-- 攻略分类 -->
<view class="category-section">
<view class="category-title">攻略分类</view>
<view class="category-grid">
<view
v-for="category in categories"
:key="category.id"
class="category-item"
@click="filterByCategory(category.id)"
:class="{ active: selectedCategory === category.id }"
>
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</view>
<!-- 攻略列表 -->
<view class="points-list-section">
<scroll-view
class="points-list"
:scroll-y="true"
:style="scrollStyle"
>
<view class="points-items">
<view
v-for="item in filteredPointsItems"
:key="item.id"
class="points-item"
@click="showPointsDetail(item)"
>
<view class="points-item-left">
<view class="points-content">
<text class="points-title">{{ item.title }}</text>
<text class="points-desc">{{ item.description }}</text>
<view class="points-reward">
<text class="reward-text">可获得: {{ item.reward }}积分</text>
</view>
</view>
</view>
<view class="points-item-right">
<text class="arrow-text"><RectRight /></text>
</view>
</view>
</view>
<!-- 空状态 -->
<view
v-if="filteredPointsItems.length === 0"
class="empty-state"
>
<text class="empty-text">暂无相关积分攻略</text>
</view>
</scroll-view>
</view>
<!-- 右侧弹出详情 -->
<nut-popup
v-model:visible="showDetailPopup"
position="right"
:style="{ width: '85%', height: '100%' }"
@close="closeDetail"
>
<view class="detail-popup">
<!-- 详情头部 -->
<view class="detail-header">
<view class="detail-title">{{ currentPointsItem?.title }}</view>
<view class="close-btn1" @click="closeDetail">
<text class="close-text">关闭</text>
</view>
</view>
<!-- 详情内容 -->
<scroll-view class="detail-content" :scroll-y="true">
<view class="detail-body">
<!-- 攻略描述 -->
<view class="detail-section">
<view class="section-title">攻略描述</view>
<text class="section-content">{{ currentPointsItem?.description }}</text>
</view>
<!-- 获取步骤 -->
<view class="detail-section">
<view class="section-title">获取步骤</view>
<view class="solution-content">
<view
v-for="(step, index) in currentPointsItem?.steps"
:key="index"
class="solution-step"
>
<view class="step-number">{{ index + 1 }}</view>
<text class="step-content">{{ step }}</text>
</view>
</view>
</view>
<!-- 积分奖励 -->
<view class="detail-section">
<view class="section-title">积分奖励</view>
<view class="reward-section">
<view class="reward-item">
<text class="reward-label">基础积分:</text>
<text class="reward-value">{{ currentPointsItem?.reward }}分</text>
</view>
<view v-if="currentPointsItem?.bonusReward" class="reward-item">
<text class="reward-label">额外奖励:</text>
<text class="reward-value bonus">{{ currentPointsItem?.bonusReward }}分</text>
</view>
</view>
</view>
<!-- 注意事项 -->
<view v-if="currentPointsItem?.notes?.length" class="detail-section">
<view class="section-title">注意事项</view>
<view class="notes-content">
<view
v-for="(note, index) in currentPointsItem.notes"
:key="index"
class="note-item"
>
<text class="note-text">• {{ note }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</nut-popup>
<!-- Toast提示 -->
<nut-toast
v-model:visible="toastVisible"
:msg="toastMessage"
:type="toastType"
/>
</view>
</template>
<script setup>
// import '@tarojs/taro/html.css'
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { RectRight } from '@nutui/icons-vue-taro'
import './index.less'
/**
* 积分攻略列表页面组件
* 功能:展示各种积分获取攻略、搜索和分类筛选
*/
// ==================== 响应式数据 ====================
/**
* 搜索关键词
*/
const searchKeyword = ref('')
/**
* 选中的分类
*/
const selectedCategory = ref('all')
/**
* 详情弹窗显示状态
*/
const showDetailPopup = ref(false)
/**
* 当前查看的积分攻略项
*/
const currentPointsItem = ref(null)
/**
* Toast提示
*/
const toastVisible = ref(false)
const toastMessage = ref('')
const toastType = ref('success')
/**
* 攻略分类数据
*/
const categories = ref([
{ id: 'all', name: '全部' },
{ id: 'daily', name: '日常任务' },
{ id: 'activity', name: '活动参与' },
{ id: 'family', name: '家庭互动' },
{ id: 'special', name: '特殊奖励' }
])
/**
* 积分攻略数据
*/
const pointsItems = ref([
{
id: 'p001',
category: 'daily',
title: '每日步数积分攻略',
description: '通过日常走路获得积分,简单易行的积分获取方式',
reward: 50,
bonusReward: 20,
steps: [
'打开微信运动,确保步数同步正常',
'每日保持至少3000步的运动量',
'在老来赛APP中同步当日步数',
'系统自动计算并发放积分奖励',
'连续7天可获得额外奖励积分'
],
notes: [
'每日步数积分上限为50分',
'步数需在当日23:59前同步',
'连续签到可获得额外奖励'
]
},
{
id: 'p002',
category: 'family',
title: '家庭成员邀请攻略',
description: '邀请家庭成员加入,共同获得积分奖励',
reward: 200,
bonusReward: 100,
steps: [
'进入家庭页面,点击邀请成员',
'分享家庭邀请码给家人',
'家人通过邀请码成功加入家庭',
'双方都可获得邀请奖励积分',
'家庭成员达到5人可获得额外奖励'
],
notes: [
'每邀请一位成员可获得200积分',
'被邀请者也可获得100积分',
'家庭成员上限为10人'
]
},
{
id: 'p003',
category: 'activity',
title: '参与社区活动攻略',
description: '参加线下社区活动,获得丰厚积分奖励',
reward: 500,
bonusReward: 300,
steps: [
'关注活动页面的最新活动信息',
'报名参加感兴趣的社区活动',
'按时到达活动地点参与活动',
'完成活动打卡和照片上传',
'活动结束后获得积分奖励'
],
notes: [
'不同活动积分奖励不同',
'需要实地参与才能获得积分',
'上传照片可获得额外积分'
]
},
{
id: 'p004',
category: 'daily',
title: '每日签到积分攻略',
description: '每天登录APP签到,轻松获得积分',
reward: 10,
bonusReward: 50,
steps: [
'每日打开老来赛APP',
'点击首页的签到按钮',
'完成当日签到获得基础积分',
'连续签到可获得递增奖励',
'连续签到7天获得额外奖励'
],
notes: [
'每日签到基础积分为10分',
'连续签到奖励递增',
'中断签到需重新开始计算'
]
},
{
id: 'p005',
category: 'family',
title: '家庭步数排行攻略',
description: '参与家庭步数排行,获得排名奖励',
reward: 100,
bonusReward: 200,
steps: [
'确保家庭成员都已加入',
'每日同步个人步数数据',
'查看家庭步数排行榜',
'努力提升个人排名',
'周末结算排名奖励积分'
],
notes: [
'排名前三可获得额外奖励',
'家庭总步数也有奖励',
'鼓励家庭成员互相督促'
]
},
{
id: 'p006',
category: 'special',
title: '轮椅陪伴运动攻略',
description: '陪伴轮椅老人运动,获得三倍积分奖励',
reward: 150,
bonusReward: 300,
steps: [
'在个人资料中标注陪伴轮椅老人',
'与轮椅老人一起进行运动',
'记录陪伴运动的时间和距离',
'上传陪伴运动的照片或视频',
'系统验证后发放三倍积分奖励'
],
notes: [
'需要提供陪伴证明',
'积分为普通运动的三倍',
'体现社会关爱精神'
]
},
{
id: 'p007',
category: 'activity',
title: '完成城市漫步攻略',
description: '参与城市漫步活动,探索城市获得积分',
reward: 300,
bonusReward: 200,
steps: [
'选择感兴趣的城市漫步路线',
'按照路线进行实地探索',
'在指定地点完成打卡',
'上传风景照片和感想',
'完成全部打卡点获得奖励'
],
notes: [
'需要完成所有打卡点',
'照片需要包含地标建筑',
'可以邀请家人一起参与'
]
},
{
id: 'p008',
category: 'special',
title: '节日特殊活动攻略',
description: '参与节日特殊活动,获得限时积分奖励',
reward: 888,
steps: [
'关注节日活动公告',
'参与节日主题活动',
'完成节日特殊任务',
'分享节日祝福内容',
'获得节日限定积分奖励'
],
notes: [
'节日活动时间有限',
'积分奖励通常较高',
'增加节日氛围和参与感'
]
}
])
/**
* 滚动样式
*/
const scrollStyle = computed(() => {
return {
height: 'calc(100vh - 260rpx)' // 减去header、搜索框、分类的高度
}
})
/**
* 过滤后的积分攻略项
*/
const filteredPointsItems = computed(() => {
let items = pointsItems.value
// 按分类过滤
if (selectedCategory.value !== 'all') {
items = items.filter(item => item.category === selectedCategory.value)
}
// 按搜索关键词过滤
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.toLowerCase()
items = items.filter(item =>
item.title.toLowerCase().includes(keyword) ||
item.description.toLowerCase().includes(keyword)
)
}
return items
})
// ==================== 方法 ====================
/**
* 返回上一页
*/
const goBack = () => {
Taro.navigateBack()
}
/**
* 处理搜索
*/
const handleSearch = () => {
// 搜索逻辑已在computed中处理
}
/**
* 按分类过滤
*/
const filterByCategory = (categoryId) => {
selectedCategory.value = categoryId
}
/**
* 显示积分攻略详情
*/
const showPointsDetail = (item) => {
currentPointsItem.value = item
showDetailPopup.value = true
}
/**
* 关闭详情
*/
const closeDetail = () => {
showDetailPopup.value = false
currentPointsItem.value = null
}
// ==================== 生命周期 ====================
onMounted(() => {
// 页面初始化逻辑
})
</script>
<script>
export default {
name: 'PointsListPage'
}
</script>