This commit is contained in:
parent
bb2f33a333
commit
d43406380c
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export default {
|
|||
mine: {
|
||||
title: 'Me',
|
||||
loginBtn: 'Login',
|
||||
points: 'Points',
|
||||
points: 'My Points',
|
||||
giftPoints: 'Gift Points',
|
||||
myCoupons: 'My Coupons',
|
||||
switchLang: 'Language',
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export default {
|
|||
mine: {
|
||||
title: '我的',
|
||||
loginBtn: '点击登录',
|
||||
points: '积分',
|
||||
points: '我的积分',
|
||||
giftPoints: '赠送积分',
|
||||
myCoupons: '我的优惠券',
|
||||
switchLang: '切换语言',
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export default {
|
|||
mine: {
|
||||
title: '我的',
|
||||
loginBtn: '點擊登入',
|
||||
points: '積分',
|
||||
points: '我的積分',
|
||||
giftPoints: '贈送積分',
|
||||
myCoupons: '我的優惠券',
|
||||
switchLang: '切換語言',
|
||||
|
|
|
|||
|
|
@ -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": "我的优惠券"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
BIN
mobile/static/ic_back2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 640 B |
BIN
mobile/static/ic_integral.png
Normal file
BIN
mobile/static/ic_integral.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Loading…
Reference in New Issue
Block a user