逻辑优化
Some checks reported errors
continuous-integration/drone/push Build encountered an error

This commit is contained in:
18631081161 2026-04-13 13:43:06 +08:00
parent bb2f33a333
commit d43406380c
20 changed files with 566 additions and 142 deletions

View File

@ -5,6 +5,10 @@ export function getMembershipProducts() {
return request.get('/membership/products')
}
export function createMembershipProduct(data: any) {
return request.post('/membership/products', data)
}
export function updateMembershipProduct(id: string, data: any) {
return request.put(`/membership/products/${id}`, data)
}

View File

@ -17,3 +17,8 @@ export function setUserMembership(uid: string, data: {
}) {
return request.put(`/user/${uid}/membership`, data)
}
// 修改用户积分
export function updateUserPoints(uid: string, points: number) {
return request.put(`/user/${uid}/points`, { points })
}

View File

@ -1,5 +1,9 @@
<template>
<div class="membership-manage">
<div class="toolbar">
<el-button type="primary" @click="handleAdd">新增会员商品</el-button>
</div>
<el-table :data="productList" border style="width: 100%">
<el-table-column label="会员类型" width="120" align="center">
<template #default="{ row }">
@ -27,10 +31,13 @@
</el-table>
<!-- 编辑弹窗 -->
<el-dialog v-model="dialogVisible" title="编辑会员商品" width="620px" destroy-on-close>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑会员商品' : '新增会员商品'" width="620px" destroy-on-close>
<el-form :model="form" label-width="130px">
<el-form-item label="会员类型">
<el-input :model-value="form.type === 'Monthly' ? '单月会员' : '订阅会员'" disabled />
<el-form-item label="会员类型" required>
<el-select v-model="form.type" style="width: 100%" :disabled="isEdit">
<el-option label="单月会员" value="Monthly" />
<el-option label="订阅会员" value="Subscription" />
</el-select>
</el-form-item>
<el-form-item label="价格" required>
<el-input-number v-model="form.price" :min="0.01" :precision="2" style="width: 100%" />
@ -72,7 +79,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import ImageUpload from '@/components/ImageUpload.vue'
import { getMembershipProducts, updateMembershipProduct } from '@/api/membership'
import { getMembershipProducts, createMembershipProduct, updateMembershipProduct } from '@/api/membership'
//
interface ProductItem {
@ -91,6 +98,7 @@ interface ProductItem {
const productList = ref<ProductItem[]>([])
const dialogVisible = ref(false)
const isEdit = ref(false)
const submitting = ref(false)
const editingId = ref('')
@ -118,8 +126,17 @@ async function loadProducts() {
}
}
//
function handleAdd() {
isEdit.value = false
editingId.value = ''
form.value = defaultForm()
dialogVisible.value = true
}
//
function handleEdit(row: ProductItem) {
isEdit.value = true
editingId.value = row.id
form.value = {
type: row.type,
@ -140,7 +157,11 @@ function handleEdit(row: ProductItem) {
async function handleSubmit() {
submitting.value = true
try {
await updateMembershipProduct(editingId.value, form.value)
if (isEdit.value) {
await updateMembershipProduct(editingId.value, form.value)
} else {
await createMembershipProduct(form.value)
}
ElMessage.success('保存成功')
dialogVisible.value = false
await loadProducts()
@ -160,4 +181,7 @@ onMounted(() => {
.membership-manage {
padding: 20px;
}
.toolbar {
margin-bottom: 16px;
}
</style>

View File

@ -65,7 +65,10 @@
<el-descriptions-item label="会员到期时间">
{{ userDetail.membershipExpireAt ? formatDate(userDetail.membershipExpireAt) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="积分余额">{{ userDetail.pointsBalance }}</el-descriptions-item>
<el-descriptions-item label="积分余额">
<span>{{ userDetail.pointsBalance }}</span>
<el-button size="small" type="primary" link style="margin-left: 8px" @click="openEditPoints">编辑</el-button>
</el-descriptions-item>
<el-descriptions-item label="注册时间">{{ formatDate(userDetail.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="语言">{{ userDetail.language }}</el-descriptions-item>
</el-descriptions>
@ -101,6 +104,19 @@
</template>
</el-dialog>
<!-- 编辑积分弹窗 -->
<el-dialog v-model="editPointsVisible" title="编辑积分" width="400px" destroy-on-close>
<el-form label-width="80px">
<el-form-item label="积分值">
<el-input-number v-model="editPointsValue" :min="0" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editPointsVisible = false">取消</el-button>
<el-button type="primary" :loading="editPointsLoading" @click="handleUpdatePoints">确定</el-button>
</template>
</el-dialog>
<!-- 设置会员弹窗 -->
<el-dialog v-model="membershipVisible" title="设为会员" width="420px" destroy-on-close>
<el-form label-width="90px">
@ -125,7 +141,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUsers, getUserDetail, setUserMembership } from '@/api/user'
import { getUsers, getUserDetail, setUserMembership, updateUserPoints } from '@/api/user'
//
interface UserItem {
@ -245,6 +261,34 @@ async function handleCancelMembership(row: UserItem) {
}
}
//
const editPointsVisible = ref(false)
const editPointsValue = ref(0)
const editPointsLoading = ref(false)
function openEditPoints() {
editPointsValue.value = userDetail.value?.pointsBalance || 0
editPointsVisible.value = true
}
async function handleUpdatePoints() {
if (!userDetail.value) return
editPointsLoading.value = true
try {
await updateUserPoints(userDetail.value.uid, editPointsValue.value)
ElMessage.success('积分已更新')
editPointsVisible.value = false
//
const res: any = await getUserDetail(userDetail.value.uid)
userDetail.value = res.data || null
await loadUsers()
} catch {
//
} finally {
editPointsLoading.value = false
}
}
onMounted(() => {
loadUsers()
})

View File

@ -30,6 +30,31 @@ public class AdminMembershipController : ControllerBase
return Ok(new { success = true, data = products });
}
/// <summary>
/// 创建会员商品
/// </summary>
[HttpPost("products")]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest request)
{
var product = new VendingMachine.Domain.Entities.MembershipProduct
{
Id = Guid.NewGuid().ToString("N")[..12],
Type = Enum.TryParse<VendingMachine.Domain.Entities.MembershipType>(request.Type, true, out var t) ? t : VendingMachine.Domain.Entities.MembershipType.Monthly,
Price = request.Price,
Currency = request.Currency,
DurationDays = request.DurationDays,
GoogleProductId = request.GoogleProductId,
AppleProductId = request.AppleProductId,
DescriptionZhCn = request.DescriptionZhCn,
DescriptionZhTw = request.DescriptionZhTw,
DescriptionEn = request.DescriptionEn
};
_db.MembershipProducts.Add(product);
await _db.SaveChangesAsync();
return Ok(new { success = true, data = product });
}
/// <summary>
/// 更新会员商品(价格、时长、宣传图、商品 ID 等)
/// </summary>
@ -68,3 +93,20 @@ public class UpdateProductRequest
public string DescriptionZhTw { get; set; } = string.Empty;
public string DescriptionEn { get; set; } = string.Empty;
}
/// <summary>
/// 创建会员商品请求体
/// </summary>
public class CreateProductRequest
{
public string Type { get; set; } = "Monthly";
public decimal Price { get; set; }
public string Currency { get; set; } = "TWD";
public int DurationDays { get; set; } = 30;
public string GoogleProductId { get; set; } = string.Empty;
public string AppleProductId { get; set; } = string.Empty;
public string DescriptionZhCn { get; set; } = string.Empty;
public string DescriptionZhTw { get; set; } = string.Empty;
public string DescriptionEn { get; set; } = string.Empty;
}

View File

@ -94,6 +94,22 @@ public class AdminUserController : ControllerBase
return Ok(new { success = true, message = request.IsMember ? "已设为会员" : "已取消会员" });
}
/// <summary>
/// 修改用户积分
/// </summary>
[HttpPut("{uid}/points")]
public async Task<IActionResult> UpdatePoints(string uid, [FromBody] UpdatePointsRequest request)
{
var user = await _db.Users.FirstOrDefaultAsync(u => u.Uid == uid);
if (user == null)
return NotFound(new { success = false, message = "用户不存在" });
user.PointsBalance = request.Points;
await _db.SaveChangesAsync();
return Ok(new { success = true, message = "积分已更新", data = new { pointsBalance = user.PointsBalance } });
}
/// <summary>
/// 获取用户详情
/// </summary>
@ -145,3 +161,13 @@ public class SetMembershipRequest
/// <summary>到期时间(可选)</summary>
public DateTime? ExpireAt { get; set; }
}
/// <summary>
/// 修改积分请求
/// </summary>
public class UpdatePointsRequest
{
/// <summary>设置的积分值</summary>
public int Points { get; set; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -75,10 +75,7 @@ async function handleGift() {
<style scoped>
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
@ -87,7 +84,7 @@ async function handleGift() {
}
.popup-content {
width: 600rpx;
width: 620rpx;
background-color: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
@ -99,21 +96,22 @@ async function handleGift() {
color: #333;
text-align: center;
display: block;
margin-bottom: 30rpx;
margin-bottom: 36rpx;
}
.popup-body {
margin-bottom: 40rpx;
margin-bottom: 36rpx;
}
.popup-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 0 24rpx;
height: 88rpx;
border: 2rpx solid #c0c8b8;
border-radius: 16rpx;
padding: 0 28rpx;
font-size: 28rpx;
margin-bottom: 20rpx;
color: #333;
margin-bottom: 24rpx;
box-sizing: border-box;
}
@ -124,27 +122,28 @@ async function handleGift() {
.btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
height: 84rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background-color: #f5f5f5;
background-color: #ffffff;
border: 2rpx solid #6b7c5e;
}
.confirm-btn {
background-color: #007aff;
background-color: #6b7c5e;
}
.btn-text {
font-size: 28rpx;
font-size: 30rpx;
}
.cancel-text {
color: #666;
color: #6b7c5e;
}
.confirm-text {

View File

@ -81,7 +81,7 @@ export default {
mine: {
title: 'Me',
loginBtn: 'Login',
points: 'Points',
points: 'My Points',
giftPoints: 'Gift Points',
myCoupons: 'My Coupons',
switchLang: 'Language',

View File

@ -81,7 +81,7 @@ export default {
mine: {
title: '我的',
loginBtn: '点击登录',
points: '积分',
points: '我的积分',
giftPoints: '赠送积分',
myCoupons: '我的优惠券',
switchLang: '切换语言',

View File

@ -81,7 +81,7 @@ export default {
mine: {
title: '我的',
loginBtn: '點擊登入',
points: '積分',
points: '我的積分',
giftPoints: '贈送積分',
myCoupons: '我的優惠券',
switchLang: '切換語言',

View File

@ -3,6 +3,7 @@
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "首页"
}
},
@ -22,6 +23,7 @@
{
"path": "pages/stamps/stamps",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "节日印花"
}
},
@ -35,12 +37,14 @@
{
"path": "pages/points/points",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的积分"
}
},
{
"path": "pages/coupons/coupons",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的优惠券"
}
},

View File

@ -1,37 +1,69 @@
<template>
<view class="coupons-page">
<!-- Tab切换 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.value"
class="tab-item"
:class="{ active: currentTab === tab.value }"
@click="switchTab(tab.value)"
>
<text class="tab-text">{{ t(tab.label) }}</text>
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back2.png" mode="aspectFit" />
</view>
<text class="nav-title">{{ t('coupons.title') }}</text>
<view class="nav-placeholder" />
</view>
</view>
<!-- 优惠券列表 -->
<view class="coupon-list">
<view
v-for="coupon in coupons"
:key="coupon.couponId"
class="coupon-item"
>
<CouponCard :coupon="coupon" :redeemed="true" />
<!-- 状态标识 -->
<view v-if="coupon.status === 'used'" class="status-badge used">
<text class="status-text">{{ t('coupons.usedLabel') }}</text>
</view>
<view v-if="coupon.status === 'expired'" class="status-badge expired">
<text class="status-text">{{ t('coupons.expiredLabel') }}</text>
<!-- 页面内容 -->
<view :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- Tab切换 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.value"
class="tab-item"
:class="{ active: currentTab === tab.value }"
@click="switchTab(tab.value)"
>
<text class="tab-text">{{ t(tab.label) }}</text>
<view v-if="currentTab === tab.value" class="tab-line" />
</view>
</view>
<!-- 空状态 -->
<EmptyState v-if="!loading && coupons.length === 0" text="暂无优惠券" />
<!-- 优惠券列表 -->
<view class="coupon-list">
<view
v-for="coupon in coupons"
:key="coupon.couponId"
class="coupon-card"
>
<image
class="coupon-bg"
:src="currentTab === 'available' ? '/static/ic_coupon_bg.png' : '/static/ic_coupon_used_bg.png'"
mode="aspectFill"
/>
<view class="coupon-inner">
<view class="coupon-info">
<text class="coupon-name">{{ coupon.name }}</text>
<view class="coupon-detail-row">
<text class="coupon-dot"></text>
<text class="coupon-detail-label">{{ t('couponCard.expireLabel') }}</text>
<text class="coupon-detail-value highlight">{{ formatExpire(coupon.expireAt) }}</text>
</view>
<view class="coupon-detail-row">
<text class="coupon-dot"></text>
<text class="coupon-detail-label">{{ t('couponCard.conditionLabel') }}</text>
<text class="coupon-detail-value highlight">{{ coupon.pointsCost ?? 0 }}{{ t('couponCard.pointsUnit') }}</text>
</view>
</view>
<view class="coupon-action" v-if="currentTab === 'available'">
<view class="redeem-btn" @click="onUse(coupon)">
<text class="redeem-btn-text">{{ t('couponCard.redeemBtn') }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<EmptyState v-if="!loading && coupons.length === 0" text="暂无优惠券" />
</view>
</view>
</view>
</template>
@ -40,11 +72,17 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { getMyCoupons } from '../../api/coupon.js'
import CouponCard from '../../components/CouponCard.vue'
import EmptyState from '../../components/EmptyState.vue'
const { t } = useI18n()
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
// Tab
const tabs = [
{ label: 'coupons.availableTab', value: 'available' },
@ -56,6 +94,11 @@ const currentTab = ref('available')
const coupons = ref([])
const loading = ref(false)
//
function goBack() {
uni.navigateBack()
}
//
async function loadCoupons() {
loading.value = true
@ -77,6 +120,18 @@ function switchTab(tab) {
loadCoupons()
}
//
function formatExpire(dateStr) {
if (!dateStr) return t('couponCard.noLimit')
const d = new Date(dateStr)
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}`
}
// 使
function onUse(coupon) {
uni.showToast({ title: '请在购买时使用', icon: 'none' })
}
//
loadCoupons()
</script>
@ -84,87 +139,153 @@ loadCoupons()
<style scoped>
.coupons-page {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #f5f0e8;
}
/* 自定义导航栏 */
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
}
.nav-back {
width: 60rpx;
height: 44px;
display: flex;
align-items: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
/* Tab栏 */
.tab-bar {
display: flex;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
flex: 1;
height: 88rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #007aff;
border-radius: 2rpx;
}
.tab-text {
font-size: 28rpx;
color: #666;
color: #999;
}
.tab-item.active .tab-text {
color: #333;
font-weight: 500;
}
.tab-line {
position: absolute;
bottom: 0;
width: 48rpx;
height: 6rpx;
background-color: #333;
border-radius: 3rpx;
}
.tab-item.active .tab-text {
color: #007aff;
/* 优惠券列表 */
.coupon-list {
padding: 24rpx 24rpx;
}
.coupon-card {
position: relative;
margin-bottom: 20rpx;
border-radius: 20rpx;
overflow: hidden;
}
.coupon-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.coupon-inner {
position: relative;
z-index: 1;
padding: 32rpx;
display: flex;
flex-direction: column;
min-height: 240rpx;
}
.coupon-info {
flex: 1;
}
.coupon-name {
font-size: 34rpx;
color: #333;
font-weight: 600;
display: block;
margin-left: 28rpx;
margin-bottom: 16rpx;
}
.coupon-detail-row {
display: flex;
align-items: center;
margin-left: 28rpx;
margin-bottom: 8rpx;
}
.coupon-dot {
font-size: 14rpx;
color: #D9DED7;
margin-right: 10rpx;
}
.coupon-detail-label {
font-size: 24rpx;
color: #666;
}
.coupon-detail-value {
font-size: 24rpx;
color: #666;
}
.coupon-detail-value.highlight {
color: #c9a96e;
font-weight: 500;
}
.coupon-list {
padding: 20rpx 24rpx;
/* 兑换按钮 */
.coupon-action {
display: flex;
justify-content: flex-end;
margin-top: 16rpx;
}
.coupon-item {
position: relative;
margin-bottom: 20rpx;
.redeem-btn {
background: linear-gradient(135deg, #d4a855, #c9a96e);
border-radius: 28rpx;
width: 172rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.status-badge {
position: absolute;
top: 20rpx;
right: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.status-badge.used {
background-color: rgba(153, 153, 153, 0.15);
}
.status-badge.expired {
background-color: rgba(255, 59, 48, 0.1);
}
.status-text {
font-size: 22rpx;
}
.status-badge.used .status-text {
color: #999;
}
.status-badge.expired .status-text {
color: #ff3b30;
}
.empty-tip {
text-align: center;
padding: 60rpx;
.redeem-btn-text {
font-size: 28rpx;
color: #999;
color: #ffffff;
white-space: nowrap;
}
</style>

View File

@ -1,5 +1,14 @@
<template>
<view class="home-page">
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<text class="nav-title">首页</text>
</view>
</view>
<!-- 页面内容 -->
<view :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- Banner轮播图 -->
<view class="banner-wrap">
<swiper class="banner-swiper" autoplay circular indicator-dots indicator-color="rgba(255,255,255,0.4)"
@ -83,6 +92,7 @@
<!-- 会员二维码弹窗 -->
<QrcodePopup :visible="showQrcode" @close="showQrcode = false" />
</view>
</view>
</template>
@ -122,6 +132,13 @@
} = useI18n()
const userStore = useUserStore()
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
//
const banners = ref([])
const entries = ref([])
@ -264,6 +281,27 @@
</script>
<style scoped>
/* 自定义导航栏 */
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.home-page {
min-height: 100vh;
background-color: #f5f0e8;

View File

@ -33,7 +33,9 @@
<!-- 积分区域 -->
<view v-if="userStore.isLoggedIn" class="points-card" @click="goPoints">
<view class="points-left">
<image class="points-icon" src="/static/ic_stamp.png" mode="aspectFit" />
<view class="points-icon-wrap">
<image class="points-icon" src="/static/ic_integral.png" mode="aspectFit" />
</view>
<text class="points-label">{{ t('mine.points') }}</text>
</view>
<text class="points-value">{{ balance }}</text>
@ -165,6 +167,8 @@ async function handleLogout() {
showLogoutConfirm.value = false
await userStore.logout()
balance.value = 0
//
uni.reLaunch({ url: '/pages/index/index' })
}
//
@ -277,11 +281,20 @@ onMounted(() => {
display: flex;
align-items: center;
}
.points-icon {
width: 48rpx;
height: 48rpx;
.points-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #f5f0e8;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.points-icon {
width: 40rpx;
height: 40rpx;
}
.points-label {
font-size: 30rpx;
color: #333;

View File

@ -1,5 +1,18 @@
<template>
<view class="points-page">
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<text class="nav-back-icon"></text>
</view>
<text class="nav-title">{{ t('points.title') }}</text>
<view class="nav-placeholder" />
</view>
</view>
<!-- 页面内容 -->
<view :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- Tab切换 -->
<view class="tab-bar">
<view
@ -33,10 +46,7 @@
<text class="record-source">{{ record.source }}</text>
<text class="record-time">{{ formatTime(record.createdAt) }}</text>
</view>
<text
class="record-amount"
:class="{ earn: currentTab === 'earn', spend: currentTab === 'spend' }"
>
<text class="record-amount">
{{ currentTab === 'earn' ? '+' : '-' }}{{ record.amount }}
</text>
</view>
@ -50,6 +60,7 @@
<text>{{ t('common.noMore') }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
@ -61,6 +72,17 @@ import EmptyState from '../../components/EmptyState.vue'
const { t } = useI18n()
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
function goBack() {
uni.navigateBack()
}
const currentTab = ref('earn')
const records = ref([])
const page = ref(1)
@ -82,8 +104,8 @@ async function loadRecords(isLoadMore = false) {
try {
const fetchFn = currentTab.value === 'earn' ? getEarnRecords : getSpendRecords
const res = await fetchFn({ page: page.value, pageSize })
const list = res.data?.records || res.data || []
const res = await fetchFn({ page: page.value, size: pageSize })
const list = res.data?.items || res.data?.records || res.data || []
if (isLoadMore) {
records.value = [...records.value, ...list]
@ -124,15 +146,48 @@ loadRecords()
<style scoped>
.points-page {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #ffffff;
display: flex;
flex-direction: column;
}
/* 自定义导航栏 */
.custom-nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
.nav-back {
width: 60rpx;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-back-icon {
font-size: 48rpx;
color: #333;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
.tab-bar {
display: flex;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
@ -152,33 +207,30 @@ loadRecords()
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #007aff;
background-color: #333;
border-radius: 2rpx;
}
.tab-text {
font-size: 30rpx;
color: #666;
color: #999;
}
.tab-item.active .tab-text {
color: #007aff;
color: #333;
font-weight: 500;
}
.record-list {
flex: 1;
padding: 20rpx 24rpx;
}
.record-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
padding: 28rpx 30rpx;
border-radius: 12rpx;
margin-bottom: 16rpx;
padding: 30rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.record-info {
@ -194,7 +246,7 @@ loadRecords()
}
.record-time {
font-size: 22rpx;
font-size: 24rpx;
color: #999;
display: block;
}
@ -202,14 +254,7 @@ loadRecords()
.record-amount {
font-size: 32rpx;
font-weight: 600;
}
.record-amount.earn {
color: #4caf50;
}
.record-amount.spend {
color: #ff3b30;
color: #333;
}
.loading-tip {

View File

@ -1,5 +1,18 @@
<template>
<view class="stamps-page">
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<text class="nav-back-icon"></text>
</view>
<text class="nav-title">{{ t('stamps.title') }}</text>
<view class="nav-placeholder" />
</view>
</view>
<!-- 页面内容 -->
<view :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- 印花Banner图 -->
<view class="banner-wrap">
<image
@ -63,6 +76,7 @@
@confirm="onRedeemConfirm"
@cancel="showRedeem = false"
/>
</view>
</view>
</template>
@ -80,6 +94,17 @@ import EmptyState from '../../components/EmptyState.vue'
const { t } = useI18n()
const userStore = useUserStore()
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
function goBack() {
uni.navigateBack()
}
//
const bannerUrl = ref('')
const stampCoupons = ref([])
@ -155,6 +180,40 @@ onMounted(() => {
background-color: #f5f0e8;
}
/* 自定义导航栏 */
.custom-nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
.nav-back {
width: 60rpx;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-back-icon {
font-size: 48rpx;
color: #333;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
/* Banner */
.banner-wrap {
padding: 24rpx 24rpx 0;

BIN
mobile/static/ic_back2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB