增加推销模式

This commit is contained in:
cyh666666
2026-03-06 17:30:50 +08:00
parent 3b0e397714
commit 7b5801a72b
39 changed files with 9831 additions and 34 deletions

View File

@@ -1,5 +1,5 @@
<template>
<view class="points-page">
<template>
<scroll-view class="points-page" scroll-y>
<view class="points-header">
<view class="points-info">
<text class="points-label">当前积分</text>
@@ -9,7 +9,53 @@
<button class="exchange-btn" @click="handleExchange">积分兑换</button>
</view>
</view>
<view class="quick-actions">
<view class="action-item" @click="goToSignin">
<view class="action-icon signin-icon">📅</view>
<text class="action-text">每日签到</text>
<view class="action-badge" v-if="!signedToday">
<text class="badge-text">+5</text>
</view>
<view class="signed-badge" v-else>
<text class="signed-text">已签</text>
</view>
</view>
<view class="action-item" @click="handleExchange">
<view class="action-icon exchange-icon">🎁</view>
<text class="action-text">积分兑换</text>
</view>
<view class="action-item" @click="goToMyReviews">
<view class="action-icon review-icon">⭐</view>
<text class="action-text">我的评价</text>
</view>
</view>
<view class="signin-card" v-if="!signedToday">
<view class="signin-info">
<text class="signin-title">今日未签到</text>
<text class="signin-desc">连续签到可获得额外奖励</text>
</view>
<button class="signin-btn" @click="goToSignin">去签到</button>
</view>
<view class="signin-card signed" v-else>
<view class="signin-info">
<text class="signin-title">今日已签到</text>
<text class="signin-desc">已连续签到 {{ continuousDays }} 天</text>
</view>
<text class="signed-icon">✓</text>
</view>
<view class="expiring-card" v-if="expiringPoints > 0" @click="showExpiringDetails">
<view class="expiring-icon">⚠️</view>
<view class="expiring-info">
<text class="expiring-title">{{ expiringPoints }} 积分即将过期</text>
<text class="expiring-date">过期日期:{{ expiringDate }}</text>
</view>
<text class="expiring-arrow"></text>
</view>
<view class="records-section">
<text class="section-title">积分明细</text>
@@ -35,7 +81,31 @@
</view>
</view>
</view>
</view>
<view class="expiring-popup" v-if="showExpiringPopup" @click="closeExpiringPopup">
<view class="popup-content" @click.stop>
<view class="popup-header">
<text class="popup-title">即将过期积分</text>
<text class="popup-close" @click="closeExpiringPopup">×</text>
</view>
<view class="popup-list">
<view class="popup-item" v-for="(detail, index) in expiringDetails" :key="index">
<view class="popup-item-info">
<text class="popup-item-points">+{{ detail.points }} 积分</text>
<text class="popup-item-desc">{{ detail.description ?? '积分获取' }}</text>
</view>
<view class="popup-item-expire">
<text class="popup-item-date">{{ formatDate(detail.expires_at) }}</text>
<text class="popup-item-label">过期</text>
</view>
</view>
</view>
<view class="popup-tip">
<text class="tip-text">积分有效期为获取后365天请及时使用避免过期</text>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
@@ -51,9 +121,22 @@ type PointRecord = {
created_at: string
}
type ExpiringDetail = {
points: number
description: string | null
expires_at: string
created_at: string
}
const totalPoints = ref<number>(0)
const records = ref<PointRecord[]>([])
const loading = ref<boolean>(true)
const signedToday = ref<boolean>(false)
const continuousDays = ref<number>(0)
const expiringPoints = ref<number>(0)
const expiringDate = ref<string>('')
const expiringDetails = ref<ExpiringDetail[]>([])
const showExpiringPopup = ref<boolean>(false)
const loadPoints = async (): Promise<void> => {
try {
@@ -67,16 +150,62 @@ const loadPoints = async (): Promise<void> => {
const loadRecords = async (): Promise<void> => {
try {
const list = await supabaseService.getPointRecords()
records.value = list as PointRecord[]
records.value = list as PointRecord[]
} catch (e) {
console.error('获取积分记录失败', e)
}
}
const loadSigninStatus = async (): Promise<void> => {
try {
const status = await supabaseService.getTodaySigninStatus()
signedToday.value = status.getBoolean('signed') ?? false
continuousDays.value = status.getNumber('continuous_days') ?? 0
} catch (e) {
console.error('获取签到状态失败', e)
}
}
const loadExpiringPoints = async (): Promise<void> => {
try {
const result = await supabaseService.getExpiringPoints()
expiringPoints.value = result.getNumber('expiring_points') ?? 0
expiringDate.value = result.getString('expiring_date') ?? ''
const detailsRaw = result.get('details')
if (detailsRaw != null && Array.isArray(detailsRaw)) {
const details: ExpiringDetail[] = []
const arr = detailsRaw as any[]
for (let i = 0; i < arr.length; i++) {
const item = arr[i]
let itemObj: UTSJSONObject
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
details.push({
points: itemObj.getNumber('points') ?? 0,
description: itemObj.getString('description'),
expires_at: itemObj.getString('expires_at') ?? '',
created_at: itemObj.getString('created_at') ?? ''
})
}
expiringDetails.value = details
}
} catch (e) {
console.error('获取即将过期积分失败', e)
}
}
const loadData = async (): Promise<void> => {
loading.value = true
await loadPoints()
await loadRecords()
await Promise.all([
loadPoints(),
loadRecords(),
loadSigninStatus(),
loadExpiringPoints()
])
loading.value = false
}
@@ -84,15 +213,33 @@ onMounted(() => {
loadData()
})
const handleExchange = () => {
uni.showToast({
title: '积分商城开发中',
icon: 'none'
const handleExchange = (): void => {
uni.navigateTo({
url: '/pages/mall/consumer/points/exchange'
})
}
const goToSignin = (): void => {
uni.navigateTo({
url: '/pages/mall/consumer/points/signin'
})
}
const goToMyReviews = (): void => {
uni.navigateTo({
url: '/pages/mall/consumer/my-reviews'
})
}
const showExpiringDetails = (): void => {
showExpiringPopup.value = true
}
const closeExpiringPopup = (): void => {
showExpiringPopup.value = false
}
const getTypeText = (type: string): string => {
// 不支持 Record<string, string>,使用 if-else
if (type == 'signin') {
return '每日签到'
} else if (type == 'shopping') {
@@ -103,6 +250,8 @@ const getTypeText = (type: string): string => {
return '系统调整'
} else if (type == 'register') {
return '注册赠送'
} else if (type == 'expire') {
return '积分过期'
} else {
return '积分变动'
}
@@ -118,15 +267,26 @@ const formatTime = (timeStr: string): string => {
const mm = date.getMinutes().toString().padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}`
}
const formatDate = (dateStr: string): string => {
if (dateStr == '') return ''
const date = new Date(dateStr)
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
return `${y}-${m}-${d}`
}
</script>
<style>
.points-page {
flex: 1;
height: 100%;
background-color: #f5f5f5;
}
.points-header {
background-color: #ff5000;
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
padding: 30px 20px;
color: white;
display: flex;
@@ -161,11 +321,174 @@ const formatTime = (timeStr: string): string => {
line-height: 32px;
}
.quick-actions {
display: flex;
flex-direction: row;
background-color: white;
padding: 16px 0;
margin-bottom: 8px;
}
.action-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.action-icon {
width: 44px;
height: 44px;
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-bottom: 6px;
}
.signin-icon {
background-color: #fff5f0;
}
.exchange-icon {
background-color: #f0f5ff;
}
.review-icon {
background-color: #fff5f0;
}
.action-text {
font-size: 12px;
color: #666;
}
.action-badge {
position: absolute;
top: 0;
right: 20px;
background-color: #ff6b35;
border-radius: 8px;
padding: 2px 6px;
}
.badge-text {
font-size: 10px;
color: white;
}
.signed-badge {
position: absolute;
top: 0;
right: 20px;
background-color: #52c41a;
border-radius: 8px;
padding: 2px 6px;
}
.signed-text {
font-size: 10px;
color: white;
}
.signin-card {
background-color: white;
margin: 0 12px 8px;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.signin-card.signed {
background: linear-gradient(135deg, #f6ffed 0%, #e6fffb 100%);
}
.signin-info {
display: flex;
flex-direction: column;
}
.signin-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.signin-desc {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.signin-btn {
background-color: #ff6b35;
color: white;
font-size: 14px;
border-radius: 16px;
padding: 0 20px;
height: 32px;
line-height: 32px;
}
.signed-icon {
width: 32px;
height: 32px;
background-color: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: white;
}
.expiring-card {
background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
margin: 0 12px 8px;
border-radius: 12px;
padding: 14px 16px;
display: flex;
flex-direction: row;
align-items: center;
}
.expiring-icon {
font-size: 24px;
margin-right: 12px;
}
.expiring-info {
flex: 1;
display: flex;
flex-direction: column;
}
.expiring-title {
font-size: 14px;
font-weight: bold;
color: #d48806;
}
.expiring-date {
font-size: 12px;
color: #ad8b00;
margin-top: 2px;
}
.expiring-arrow {
font-size: 20px;
color: #d48806;
}
.records-section {
background-color: white;
margin-top: 10px;
padding: 0 16px;
min-height: 500px;
min-height: 300px;
}
.section-title {
@@ -187,7 +510,10 @@ const formatTime = (timeStr: string): string => {
.record-left {
display: flex;
flex-direction: column;
margin-bottom: 4px;
}
.record-title {
margin-bottom: 4px;
font-size: 15px;
color: #333;
}
@@ -204,7 +530,7 @@ const formatTime = (timeStr: string): string => {
}
.record-amount.positive {
color: #ff5000;
color: #ff6b35;
}
.record-amount.negative {
@@ -221,4 +547,108 @@ const formatTime = (timeStr: string): string => {
color: #999;
font-size: 14px;
}
</style>
.loading-state {
padding: 40px 0;
display: flex;
justify-content: center;
}
.expiring-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
}
.popup-content {
background-color: white;
border-radius: 16px 16px 0 0;
width: 100%;
max-height: 60%;
padding: 16px;
}
.popup-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.popup-close {
font-size: 24px;
color: #999;
}
.popup-list {
max-height: 300px;
}
.popup-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.popup-item-info {
display: flex;
flex-direction: column;
}
.popup-item-points {
font-size: 14px;
font-weight: bold;
color: #ff6b35;
}
.popup-item-desc {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.popup-item-expire {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.popup-item-date {
font-size: 12px;
color: #d48806;
}
.popup-item-label {
font-size: 10px;
color: #999;
}
.popup-tip {
margin-top: 16px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 8px;
}
.tip-text {
font-size: 12px;
color: #666;
}
</style>