feat: add points center design page

This commit is contained in:
hajimi
2026-08-03 02:41:40 +08:00
parent 348ffa8db4
commit 27bd43b3e0
4 changed files with 482 additions and 2 deletions
@@ -0,0 +1,471 @@
<template>
<view class="points-center">
<view class="points-center__nav">
<view class="points-center__back" @tap="goBack"></view>
<text class="points-center__title">积分中心</text>
<view class="points-center__rules" @tap="openRules">积分规则</view>
</view>
<scroll-view scroll-y class="points-center__scroll">
<view class="points-card">
<view class="points-card__rings points-card__rings--one" />
<view class="points-card__rings points-card__rings--two" />
<view class="points-card__rings points-card__rings--three" />
<view class="points-card__content">
<text class="points-card__label">可用积分</text>
<text class="points-card__value">{{ userPoints }}</text>
<text class="points-card__desc">用于解锁社区付费内容</text>
</view>
<view class="points-card__detail" @tap="openDetails">
<text class="points-card__detail-icon"></text>
<text>积分明细</text>
</view>
</view>
<view class="quick-grid">
<view v-for="item in quickActions" :key="item.key" class="quick-item" @tap="handleQuickAction(item.key)">
<view class="quick-item__icon" :class="`quick-item__icon--${item.tone}`">{{ item.icon }}</view>
<text class="quick-item__label">{{ item.label }}</text>
<text class="quick-item__arrow"></text>
</view>
</view>
<view class="section-heading">
<text class="section-heading__title">最近明细</text>
<view class="section-heading__more" @tap="openDetails">
<text>全部明细</text>
<text class="section-heading__arrow"></text>
</view>
</view>
<view class="detail-card">
<view v-if="loading" class="detail-state">正在加载明细</view>
<view v-else-if="!recentLogs.length" class="detail-state">暂无积分明细</view>
<view v-else v-for="(item, index) in recentLogs" :key="item.id || `${item.create_time}-${index}`" class="detail-item">
<view class="detail-item__icon" :class="`detail-item__icon--${detailTone(item)}`">{{ detailIcon(item) }}</view>
<view class="detail-item__body">
<text class="detail-item__title">{{ detailTitle(item) }}</text>
<text class="detail-item__time">{{ item.create_time }}</text>
</view>
<view class="detail-item__amount" :class="{ 'detail-item__amount--income': isIncome(item) }">
<text>{{ item.change_amount_desc }}</text>
<text class="detail-item__status">{{ isIncome(item) ? '已到账' : '已完成' }}</text>
</view>
</view>
</view>
<view class="points-tip">
<text class="points-tip__icon"></text>
<text>积分不可用于提现奖励以规则及审核结果为准</text>
</view>
<view class="points-center__bottom-button" @tap="openDetails">查看全部明细</view>
<view class="points-center__safe-area" />
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { accountLog } from '@/api/user'
import request from '@/utils/request'
interface PointLog {
id?: number
action?: number
change_amount_desc?: string
change_type?: string
create_time?: string
remark?: string
type_desc?: string
}
const userPoints = ref(0)
const recentLogs = ref<PointLog[]>([])
const loading = ref(false)
const quickActions = [
{ key: 'earn', label: '获取积分', icon: '+', tone: 'blue' },
{ key: 'unlocked', label: '已解锁内容', icon: '♙', tone: 'blue' },
{ key: 'invite', label: '邀请记录', icon: '♟', tone: 'blue' },
{ key: 'guide', label: '使用说明', icon: '▤', tone: 'blue' }
]
const isIncome = (item: PointLog) => Number(item.action) !== 2
const detailTitle = (item: PointLog) => item.remark || item.type_desc || '积分变动'
const detailTone = (item: PointLog) => isIncome(item) ? 'income' : 'expense'
const detailIcon = (item: PointLog) => isIncome(item) ? '♟' : '♙'
const fetchData = async () => {
loading.value = true
try {
const [stats, logs] = await Promise.all([
request.get<any>({ url: '/community/userStats' }, { isTransformResponse: false }),
accountLog({ action: '', type: 'up', page_no: 1, page_size: 3 })
])
if (stats?.code === 1) userPoints.value = Number(stats.data?.user_points || 0)
recentLogs.value = Array.isArray(logs?.lists) ? logs.lists : []
} catch (error) {
recentLogs.value = []
} finally {
loading.value = false
}
}
const goBack = () => uni.navigateBack()
const openRules = () => uni.navigateTo({ url: '/pages/agreement/agreement?type=points_rule' })
const openDetails = () => uni.navigateTo({ url: '/packages/pages/points_log/points_log' })
const handleQuickAction = (key: string) => {
if (key === 'earn') return uni.navigateTo({ url: '/packages/pages/points_products/points_products' })
if (key === 'invite') return uni.navigateTo({ url: '/pages/distribution/distribution' })
if (key === 'unlocked') return uni.switchTab({ url: '/pages/community/community' })
openRules()
}
onShow(fetchData)
</script>
<style lang="scss" scoped>
$blue: #0869ed;
$text: #111827;
$muted: #8b929e;
$line: #e8ebef;
.points-center {
height: 100vh;
overflow: hidden;
background: #fff;
color: $text;
&__nav {
height: 104rpx;
padding: 0 36rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
&__back {
width: 80rpx;
color: #111827;
font-size: 76rpx;
font-family: Arial, sans-serif;
font-weight: 200;
line-height: 70rpx;
transform: translateY(-4rpx);
}
&__title {
font-size: 40rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
&__rules {
width: 150rpx;
color: $blue;
font-size: 30rpx;
text-align: right;
}
&__scroll {
height: calc(100vh - 104rpx);
box-sizing: border-box;
}
&__bottom-button {
margin: 28rpx 38rpx 0;
height: 92rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 28rpx;
color: #fff;
background: linear-gradient(100deg, #0869ed, #277ff1);
box-shadow: 0 12rpx 26rpx rgba(8, 105, 237, .18);
font-size: 32rpx;
font-weight: 700;
}
&__safe-area {
height: 28rpx;
}
}
.points-card {
position: relative;
height: 304rpx;
margin: 0 36rpx 20rpx;
overflow: hidden;
border-radius: 28rpx;
color: #fff;
background: linear-gradient(116deg, #0969eb 0%, #3e8ff5 60%, #86baf8 100%);
&__content {
position: relative;
z-index: 2;
padding: 42rpx 0 0 50rpx;
}
&__label {
display: block;
font-size: 32rpx;
font-weight: 600;
}
&__value {
display: block;
margin-top: 20rpx;
font-size: 82rpx;
line-height: 1;
font-weight: 800;
}
&__desc {
display: block;
margin-top: 22rpx;
font-size: 28rpx;
}
&__detail {
position: absolute;
right: 34rpx;
bottom: 44rpx;
z-index: 3;
height: 68rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
gap: 14rpx;
border-radius: 22rpx;
color: $blue;
background: #fff;
box-shadow: 0 10rpx 20rpx rgba(4, 65, 150, .14);
font-size: 28rpx;
font-weight: 600;
}
&__detail-icon {
font-size: 34rpx;
line-height: 1;
}
&__rings {
position: absolute;
z-index: 1;
border: 1rpx solid rgba(255, 255, 255, .16);
border-radius: 50%;
}
&__rings--one {
width: 620rpx;
height: 620rpx;
right: -150rpx;
top: 44rpx;
}
&__rings--two {
width: 470rpx;
height: 470rpx;
right: -55rpx;
top: 118rpx;
border-color: rgba(255, 255, 255, .11);
}
&__rings--three {
width: 330rpx;
height: 330rpx;
right: 28rpx;
top: 188rpx;
border-color: rgba(255, 255, 255, .08);
}
}
.quick-grid {
margin: 0 36rpx 32rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
overflow: hidden;
border: 1rpx solid $line;
border-radius: 26rpx;
}
.quick-item {
position: relative;
height: 144rpx;
padding: 0 42rpx;
display: flex;
align-items: center;
gap: 26rpx;
box-sizing: border-box;
&:nth-child(odd) {
border-right: 1rpx solid $line;
}
&:nth-child(-n + 2) {
border-bottom: 1rpx solid $line;
}
&__icon {
width: 70rpx;
height: 70rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: $blue;
background: #edf4ff;
font-size: 48rpx;
line-height: 1;
}
&__label {
font-size: 30rpx;
font-weight: 600;
white-space: nowrap;
}
&__arrow {
margin-left: auto;
color: #8b929e;
font-size: 46rpx;
font-family: Arial, sans-serif;
font-weight: 200;
}
}
.section-heading {
margin: 0 46rpx 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
&__title {
font-size: 34rpx;
font-weight: 700;
}
&__more {
display: flex;
align-items: center;
color: $muted;
font-size: 26rpx;
}
&__arrow {
margin-left: 8rpx;
font-size: 38rpx;
line-height: 1;
}
}
.detail-card {
margin: 0 36rpx;
padding: 0 30rpx;
overflow: hidden;
border: 1rpx solid $line;
border-radius: 26rpx;
}
.detail-item {
min-height: 116rpx;
padding: 22rpx 0;
display: flex;
align-items: center;
box-sizing: border-box;
& + & {
border-top: 1rpx solid $line;
}
&__icon {
width: 70rpx;
height: 70rpx;
margin-right: 26rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20rpx;
font-size: 40rpx;
}
&__icon--expense {
color: $blue;
background: #edf4ff;
}
&__icon--income {
color: #32b76a;
background: #eaf9ee;
}
&__body {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
&__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 28rpx;
font-weight: 600;
}
&__time,
&__status {
color: $muted;
font-size: 24rpx;
}
&__amount {
margin-left: 18rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
color: #151922;
font-size: 30rpx;
font-weight: 700;
}
&__amount--income {
color: $blue;
}
}
.detail-state {
padding: 64rpx 0;
color: $muted;
font-size: 26rpx;
text-align: center;
}
.points-tip {
margin: 20rpx 40rpx 0;
min-height: 68rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
gap: 14rpx;
border-radius: 20rpx;
color: #738096;
background: #eef5ff;
font-size: 24rpx;
&__icon {
color: $blue;
font-size: 32rpx;
line-height: 1;
}
}
</style>
+9
View File
@@ -345,6 +345,15 @@
"auth": true "auth": true
} }
}, },
{
"path": "pages/points_center/points_center",
"style": {
"navigationStyle": "custom"
},
"meta": {
"auth": true
}
},
{ {
"path": "pages/points_log/points_log", "path": "pages/points_log/points_log",
"style": { "style": {
+1 -1
View File
@@ -154,7 +154,7 @@ const openPoints = () => {
uni.navigateTo({ url: '/pages/login/login' }) uni.navigateTo({ url: '/pages/login/login' })
return return
} }
uni.navigateTo({ url: '/packages/pages/points_log/points_log' }) uni.navigateTo({ url: '/packages/pages/points_center/points_center' })
} }
const loadUserPoints = async () => { const loadUserPoints = async () => {
+1 -1
View File
@@ -265,7 +265,7 @@ const openPointsBuy = () => {
uni.navigateTo({ url: '/pages/login/login' }) uni.navigateTo({ url: '/pages/login/login' })
return return
} }
uni.navigateTo({ url: '/packages/pages/points_log/points_log' }) uni.navigateTo({ url: '/packages/pages/points_center/points_center' })
} }
const openChatList = () => { const openChatList = () => {