Compare commits

...

4 Commits

Author SHA1 Message Date
6f7207da2d feat: 优化UI设计并重构样式系统
refactor(components): 重构ElCard、FlipGrid、YifanSelector和PaymentPopup组件样式
refactor(pages): 优化地址管理、商品详情、订单列表、积分记录和活动页面UI
style: 更新uni.scss全局样式变量和设计系统
docs: 添加说明文档记录UI优化进度
2025-12-17 14:32:55 +08:00
dd12d8e500 feat: add app sharing and enhance UI with animations and revamped coupon display. 2025-12-17 01:55:23 +08:00
f8b1f57cd7 feat: Store openid on login, redesign mine page coupon popup, and update mine page menu items. 2025-12-16 21:04:18 +08:00
8a6ac48d59 feat: Overhaul homepage UI with new activity sections and add dedicated activity, shop, and registration pages. 2025-12-15 20:16:47 +08:00
27 changed files with 7319 additions and 2111 deletions

45
App.vue
View File

@ -17,6 +17,49 @@
}
</script>
<style>
<style lang="scss">
/*每个页面公共css */
@import '@/uni.scss';
page {
background-color: $bg-page;
color: $text-main;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
.container {
padding: $spacing-lg;
}
/* Global Keyframes */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-30rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(100%); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes zoomIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-20rpx); }
100% { transform: translateY(0); }
}
</style>

View File

@ -5,8 +5,8 @@ export function wechatLogin(code, invite_code) {
return request({ url: '/api/app/users/weixin/login', method: 'POST', data })
}
export function getInventory(user_id, page = 1, page_size = 20){
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size } })
export function getInventory(user_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size } })
}
export function bindPhone(user_id, code, extraHeader = {}) {
@ -87,8 +87,10 @@ export function requestShipping(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data: { inventory_ids: ids } })
}
export function getItemCards(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/item_cards`, method: 'GET' })
export function getItemCards(user_id, status) {
const data = {}
if (status !== undefined) data.status = status
return authRequest({ url: `/api/app/users/${user_id}/item_cards`, method: 'GET', data })
}
export function getUserCoupons(user_id, status, page = 1, page_size = 20) {
@ -97,6 +99,18 @@ export function getUserCoupons(user_id, status, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/coupons`, method: 'GET', data })
}
export function getCouponStats(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/coupons/stats`, method: 'GET' })
}
export function getCouponUsage(user_id, user_coupon_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/coupons/${user_coupon_id}/usage`, method: 'GET', data: { page, page_size } })
}
export function redeemCouponByPoints(user_id, coupon_id) {
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-coupon`, method: 'POST', data: { coupon_id } })
}
export function redeemCoupon(user_id, code) {
return authRequest({ url: `/api/app/users/${user_id}/coupons/redeem`, method: 'POST', data: { code } })
}
@ -128,3 +142,24 @@ export function getTasks(page = 1, page_size = 20) {
export function getShipments(user_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/shipments`, method: 'GET', data: { page, page_size } })
}
// 获取用户邀请记录
export function getUserInvites(user_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/invites`, method: 'GET', data: { page, page_size } })
}
// ============================================
// 兼容性适配接口 (适配 pages/mine/index.vue)
// ============================================
export function getUserInfo() {
const user_info = uni.getStorageSync('user_info')
if (user_info) return Promise.resolve(user_info)
return authRequest({ url: '/api/app/users/info', method: 'GET' })
}
export const getUserTasks = getTasks
export function getInviteRecords(page = 1, page_size = 20) {
const user_id = uni.getStorageSync('user_id')
return getUserInvites(user_id, page, page_size)
}

View File

@ -20,11 +20,36 @@ const shadowClass = computed(() => {
})
</script>
<style scoped>
.ep-card { background: #fff; border-radius: 12rpx; overflow: hidden; border: 1rpx solid #eee }
.is-always-shadow { box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06) }
.is-hover-shadow { box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06) }
.is-no-shadow { box-shadow: none }
.ep-card__header { padding: 12rpx 16rpx; border-bottom: 1rpx solid #f0f0f0 }
.ep-card__body { padding: 12rpx 16rpx }
<style lang="scss" scoped>
.ep-card {
background: $bg-card;
border: 1rpx solid $border-color-light;
border-radius: $radius-lg;
overflow: hidden;
transition: all 0.3s ease;
}
.is-always-shadow {
box-shadow: $shadow-sm;
}
.is-hover-shadow {
/* 在移动端hover效果不如PC明显这里可以处理为点击态 */
box-shadow: $shadow-sm;
}
.is-no-shadow {
box-shadow: none;
}
.ep-card__header {
padding: $spacing-sm $spacing-md;
border-bottom: 1rpx solid $border-color-light;
font-weight: 600;
color: $text-main;
}
.ep-card__body {
padding: $spacing-md;
}
</style>

View File

@ -24,7 +24,7 @@
</template>
<script setup>
import { ref, defineExpose } from 'vue'
import { ref } from 'vue'
const props = defineProps({ rewards: { type: Array, default: () => [] }, controls: { type: Boolean, default: true } })
const emit = defineEmits(['draw'])
@ -64,20 +64,143 @@ function onPreview(cell) {
defineExpose({ revealResults, reset })
</script>
<style scoped>
.flip-root { display: flex; flex-direction: column; gap: 16rpx; padding: 16rpx }
.flip-actions { display: flex; gap: 12rpx }
.flip-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
.flip-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12rpx }
.flip-card { perspective: 1000px }
.flip-inner { position: relative; width: 100%; height: 200rpx; transform-style: preserve-3d; transition: transform 0.5s }
.flip-card.flipped .flip-inner { transform: rotateY(180deg) }
.flip-front, .flip-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 12rpx; overflow: hidden }
.flip-front { background: #e2e8f0; display: flex; align-items: center; justify-content: center }
.front-placeholder { width: 80%; height: 80%; border-radius: 12rpx; background: linear-gradient(135deg, #f8fafc, #e2e8f0) }
.flip-back { background: #fff; transform: rotateY(180deg); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 12rpx }
.flip-image { width: 80%; border-radius: 8rpx; margin-bottom: 8rpx; background: #f5f5f5 }
.flip-title { font-size: 26rpx; color: #222; text-align: center; max-width: 90%; word-break: break-all }
.flip-toolbar { display: flex; justify-content: flex-end }
.flip-reset { background: #ffd166; color: #6b4b1f; border-radius: 999rpx }
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 翻牌动画组件
采用暖橙色调的开箱效果
============================================ */
.flip-root {
display: flex;
flex-direction: column;
gap: $spacing-md;
padding: $spacing-md;
}
.flip-actions {
display: flex;
gap: $spacing-sm;
}
.flip-btn {
flex: 1;
background: $gradient-brand !important;
color: #FFFFFF !important;
border-radius: $radius-md;
font-weight: 600;
box-shadow: $shadow-md;
border: none;
font-size: $font-md;
transition: all 0.2s ease;
&:active {
transform: scale(0.97);
box-shadow: $shadow-sm;
}
}
.flip-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $spacing-sm;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border-radius: $radius-lg;
padding: $spacing-sm;
box-shadow: inset 0 0 20rpx rgba(255, 255, 255, 0.5);
}
.flip-card {
perspective: 1000px;
}
.flip-inner {
position: relative;
width: 100%;
height: 220rpx;
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.flip-card.flipped .flip-inner {
transform: rotateY(180deg);
}
.flip-front, .flip-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: $radius-md;
overflow: hidden;
}
.flip-front {
background: linear-gradient(145deg, #FFF8F3, #FFE8D1);
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba($brand-primary, 0.2);
box-shadow: $shadow-sm;
}
.front-placeholder {
width: 60%;
height: 60%;
border-radius: $radius-md;
background: linear-gradient(135deg, rgba($brand-primary, 0.3), rgba($brand-primary-light, 0.2));
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
}
.flip-back {
background: $bg-card;
transform: rotateY(180deg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-sm;
border: 2rpx solid rgba($brand-primary, 0.3);
box-shadow: $shadow-md;
}
.flip-image {
width: 75%;
border-radius: $radius-sm;
margin-bottom: $spacing-xs;
background: linear-gradient(145deg, #FFF8F3, #FFF4E6);
}
.flip-title {
font-size: $font-xs;
font-weight: 600;
color: $text-main;
text-align: center;
max-width: 90%;
word-break: break-all;
line-height: 1.3;
}
.flip-toolbar {
display: flex;
justify-content: flex-end;
}
.flip-reset {
background: linear-gradient(135deg, $accent-gold, $brand-primary-light) !important;
color: #6b4b1f !important;
border-radius: 999rpx;
font-weight: 600;
box-shadow: $shadow-sm;
font-size: $font-sm;
padding: 0 40rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
}
}
</style>

View File

@ -133,14 +133,19 @@ function handleConfirm() {
}
</script>
<style scoped>
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 支付弹窗组件
采用暖橙色调的底部弹窗设计
============================================ */
.payment-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
background-color: rgba(0, 0, 0, 0.55);
z-index: 999;
display: flex;
align-items: flex-end;
@ -148,124 +153,168 @@ function handleConfirm() {
.payment-popup-content {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 30rpx;
padding-bottom: calc(30rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
background: $bg-card;
border-radius: $radius-xl $radius-xl 0 0;
padding: $spacing-lg;
padding-bottom: calc($spacing-lg + constant(safe-area-inset-bottom));
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.risk-warning {
background-color: #fffbe6;
color: #ed6a0c;
font-size: 24rpx;
padding: 16rpx 24rpx;
border-radius: 8rpx;
margin-bottom: 24rpx;
background: linear-gradient(135deg, #FFF8F3, #FFF4E6);
color: #B45309;
font-size: $font-xs;
padding: $spacing-sm $spacing-md;
border-radius: $radius-md;
margin-bottom: $spacing-md;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
line-height: 1.4;
line-height: 1.5;
text-align: center;
border: 1rpx solid rgba(255, 159, 67, 0.2);
}
.agreement-link {
color: #1890ff;
text-decoration: underline;
color: $brand-primary;
font-weight: 500;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
margin-bottom: $spacing-lg;
position: relative;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
font-size: $font-lg;
font-weight: 700;
color: $text-main;
}
.close-icon {
position: absolute;
right: 30rpx;
font-size: 40rpx;
color: #999;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: 48rpx;
color: $text-placeholder;
line-height: 1;
padding: 10rpx;
transition: color 0.2s ease;
}
.close-icon:active {
color: $text-secondary;
}
.popup-body {
padding: 30rpx;
padding: $spacing-sm 0 $spacing-md;
}
.amount-section {
text-align: center;
margin-bottom: 40rpx;
margin-bottom: $spacing-xl;
padding: $spacing-md;
background: linear-gradient(145deg, #FFFFFF, #FFF8F3);
border-radius: $radius-lg;
border: 1rpx solid rgba(255, 159, 67, 0.1);
}
.amount-section .label {
font-size: 28rpx;
color: #666;
margin-right: 10rpx;
font-size: $font-sm;
color: $text-secondary;
margin-right: $spacing-xs;
}
.amount-section .amount {
font-size: 48rpx;
font-weight: bold;
color: #ff4d4f;
font-size: 56rpx;
font-weight: 800;
background: $gradient-brand;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-item {
margin-bottom: 30rpx;
margin-bottom: $spacing-md;
}
.form-item .label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-size: $font-sm;
color: $text-main;
font-weight: 600;
margin-bottom: $spacing-xs;
}
.picker-display {
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
border: 2rpx solid $border-color-light;
border-radius: $radius-md;
padding: $spacing-md;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
background: #f9f9f9;
font-size: $font-sm;
background: $bg-page;
transition: all 0.2s ease;
}
.picker-display:active {
border-color: $brand-primary;
background: #FFF8F3;
}
.selected-text {
color: #333;
color: $text-main;
font-weight: 500;
}
.placeholder {
color: #999;
color: $text-placeholder;
}
.arrow {
color: #ccc;
color: $text-placeholder;
width: 16rpx;
height: 16rpx;
border-right: 2rpx solid #ccc;
border-bottom: 2rpx solid #ccc;
border-right: 3rpx solid $text-placeholder;
border-bottom: 3rpx solid $text-placeholder;
transform: rotate(-45deg);
margin-right: 8rpx;
margin-right: $spacing-xs;
}
.popup-footer {
display: flex;
border-top: 1rpx solid #eee;
gap: $spacing-md;
margin-top: $spacing-sm;
}
.btn-cancel, .btn-confirm {
flex: 1;
border: none;
background: #fff;
border-radius: 0;
font-size: 30rpx;
padding: 24rpx 0;
border-radius: $radius-lg;
font-size: $font-md;
padding: $spacing-md 0;
line-height: 1.5;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-cancel::after, .btn-confirm::after {
border: none;
}
.btn-cancel {
color: #666;
border-right: 1rpx solid #eee;
color: $text-secondary;
background: #F3F4F6;
}
.btn-cancel:active {
background: $border-color-light;
}
.btn-confirm {
color: #007AFF;
font-weight: bold;
color: #FFFFFF;
background: $gradient-brand;
box-shadow: $shadow-lg;
}
.btn-confirm:active {
transform: scale(0.97);
box-shadow: $shadow-md;
}
</style>

View File

@ -34,8 +34,8 @@
</view>
<view class="action-buttons">
<button v-if="selectedItems.length === 0" class="btn-random" @tap="handleRandomOne">随机一发</button>
<button v-else class="btn-buy" @tap="handleBuy">去支付</button>
<button v-if="selectedItems.length === 0" class="btn-common btn-random" @tap="handleRandomOne">随机一发</button>
<button v-else class="btn-common btn-buy" @tap="handleBuy">去支付</button>
</view>
</view>
</view>
@ -290,122 +290,255 @@ async function onPaymentConfirm(paymentData) {
}
</script>
<style scoped>
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 选号组件 (适配高级卡片布局)
============================================ */
/* 容器 - 去除背景,融入父级卡片 */
.choice-grid-container {
padding: 20rpx;
padding: $spacing-xs 0;
background: transparent;
}
/* 加载和空状态 */
.loading-state, .empty-state {
text-align: center;
padding: 40rpx;
color: #999;
padding: 80rpx 0;
color: $text-placeholder;
font-size: $font-sm;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
&::before {
content: '📦';
font-size: 60rpx;
opacity: 0.5;
animation: float 3s ease-in-out infinite;
}
}
/* 网格包装 */
.grid-wrapper {
padding-bottom: 200rpx; /* 留出底部操作栏空间 */
padding: 0 20rpx 200rpx;
}
/* 号码网格 - 调整为更合理的列数,适配不同屏幕 */
.choices-grid {
display: grid;
grid-template-columns: repeat(5, 1fr); /* 一行5个 */
grid-template-columns: repeat(6, 1fr); /* 默认6列更大尺寸 */
gap: 16rpx;
margin-bottom: 120rpx; /* 留出底部操作栏空间 */
padding: 10rpx 0;
animation: scaleIn 0.4s ease-out backwards;
}
/* 单个号码格子 */
.choice-item {
aspect-ratio: 1;
background: #fff;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
background: $bg-card;
border: 1rpx solid $border-color-light;
border-radius: $radius-md;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: $shadow-sm;
/* 票据孔装饰 */
&::before, &::after {
content: '';
position: absolute;
width: 8rpx;
height: 8rpx;
background: $bg-page; /* 与背景色一致 */
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
opacity: 0.5;
}
&::before { left: -4rpx; }
&::after { right: -4rpx; }
}
.choice-item:active {
transform: scale(0.92);
}
/* 号码文字 */
.choice-number {
font-size: 32rpx;
font-weight: bold;
color: #333;
font-size: 28rpx;
font-weight: 800;
color: $text-sub;
z-index: 1;
font-family: 'DIN Alternate', sans-serif;
transition: color 0.2s;
}
/* 状态文字 - 简化为小点或隐藏 */
.choice-status {
font-size: 20rpx;
margin-top: 4rpx;
color: #666;
display: none;
}
/* 状态样式 */
/* ============= 状态样式 ============= */
/* 可选状态 */
.is-available {
background: #fff;
background: #FFFFFF;
&:hover {
border-color: $brand-primary;
}
}
/* 已售状态 */
.is-sold {
background: #f5f5f5;
border-color: #eee;
opacity: 0.6;
}
.is-sold .choice-number {
color: #ccc;
text-decoration: line-through;
color: $text-disabled;
border-color: $border-color-light;
background: $bg-secondary;
opacity: 0.8;
pointer-events: none;
&::before {
content: 'SOLD';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-30deg);
font-size: 20rpx;
font-weight: 900;
color: $text-disabled;
opacity: 0.2;
letter-spacing: 2rpx;
border: 2rpx solid $text-disabled;
padding: 2rpx 6rpx;
border-radius: 4rpx;
z-index: 0;
}
}
.is-sold .choice-number {
color: $text-disabled;
opacity: 0.3;
}
/* 选中状态 - 橙色高亮 */
.is-selected {
background: #e6f7ff;
border-color: #1890ff;
background: $gradient-brand;
border-color: transparent;
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.3);
transform: scale(1.08);
z-index: 2;
&::before, &::after {
display: none; /* 选中状态隐藏票据孔 */
}
}
.is-selected .choice-number {
color: #1890ff;
}
.is-selected .choice-status {
color: #1890ff;
color: #FFFFFF;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
}
/* 底部操作栏 */
/* ============= 底部操作栏 ============= */
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
bottom: calc(40rpx + env(safe-area-inset-bottom));
left: 30rpx;
right: 30rpx;
background: rgba($bg-card, 0.9);
backdrop-filter: blur(20rpx);
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
box-shadow: $shadow-lg;
border-radius: 999rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
z-index: 100;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border: 1rpx solid rgba($bg-card, 0.5);
animation: slideUp 0.4s ease-out backwards;
}
/* 选择信息行 */
.selection-info {
font-size: 28rpx;
color: #333;
font-size: 26rpx;
color: $text-main;
display: flex;
align-items: center;
font-weight: 600;
}
.highlight {
color: #ff4d4f;
font-weight: bold;
margin: 0 4rpx;
color: $brand-primary;
font-weight: 800;
font-size: 36rpx;
margin: 0 8rpx;
}
/* 按钮组 */
.action-buttons {
display: flex;
gap: 20rpx;
}
/* 通用按钮样式 */
.btn-common {
height: 80rpx;
line-height: 80rpx;
padding: 0 48rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:active {
transform: scale(0.96);
}
}
/* 购买按钮 */
.btn-buy {
background: #ff4d4f;
color: #fff;
border-radius: 40rpx;
padding: 0 60rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
margin: 0;
background: $gradient-brand !important;
color: #FFFFFF !important;
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.3);
/* 脉冲动画 */
animation: pulse 2s infinite;
}
/* 随机按钮 */
.btn-random {
background: #007AFF;
color: #fff;
border-radius: 40rpx;
padding: 0 60rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
margin: 0;
background: $bg-secondary !important;
color: $text-main !important;
box-shadow: none;
border: 1rpx solid transparent;
&:active {
background: #E5E7EB !important;
}
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba($brand-primary, 0.4); }
70% { box-shadow: 0 0 0 20rpx rgba($brand-primary, 0); }
100% { box-shadow: 0 0 0 0 rgba($brand-primary, 0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
</style>

View File

@ -5,105 +5,108 @@
"style": {
"navigationBarTitleText": "uni-app"
}
}
,
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
}
,
},
{
"path": "pages/shop/index",
"style": {
"navigationBarTitleText": "商城"
}
}
,
},
{
"path": "pages/shop/detail",
"style": {
"navigationBarTitleText": "商品详情"
}
}
,
},
{
"path": "pages/cabinet/index",
"style": {
"navigationBarTitleText": "货柜"
}
}
,
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
}
,
},
{
"path": "pages/points/index",
"style": {
"navigationBarTitleText": "积分记录"
}
}
,
},
{
"path": "pages/orders/index",
"style": {
"navigationBarTitleText": "我的订单"
}
}
,
},
{
"path": "pages/address/index",
"style": {
"navigationBarTitleText": "地址管理"
}
}
,
},
{
"path": "pages/address/edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
}
,
},
{
"path": "pages/help/index",
"style": {
"navigationBarTitleText": "使用帮助"
}
}
,
},
{
"path": "pages/agreement/user",
"style": {
"navigationBarTitleText": "用户协议"
}
}
,
},
{
"path": "pages/agreement/purchase",
"style": {
"navigationBarTitleText": "购买协议"
}
}
,
},
{
"path": "pages/activity/yifanshang/index",
"style": { "navigationBarTitleText": "一番赏" }
}
,
"style": {
"navigationBarTitleText": "一番赏"
}
},
{
"path": "pages/activity/wuxianshang/index",
"style": { "navigationBarTitleText": "无限赏" }
}
,
"style": {
"navigationBarTitleText": "无限赏"
}
},
{
"path": "pages/activity/duiduipeng/index",
"style": { "navigationBarTitleText": "对对碰" }
"style": {
"navigationBarTitleText": "对对碰"
}
},
{
"path": "pages/activity/list/index",
"style": {
"navigationBarTitleText": "活动列表"
}
},
{
"path": "pages/activity/pata/index",
"style": {
"navigationBarTitleText": "爬塔"
}
},
{
"path": "pages/register/register",
@ -118,10 +121,30 @@
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{ "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tab/home.png", "selectedIconPath": "static/tab/home_active.png" },
{ "pagePath": "pages/shop/index", "text": "商城", "iconPath": "static/tab/shop.png", "selectedIconPath": "static/tab/shop_active.png" },
{ "pagePath": "pages/cabinet/index", "text": "货柜", "iconPath": "static/tab/box.png", "selectedIconPath": "static/tab/box_active.png" },
{ "pagePath": "pages/mine/index", "text": "我的", "iconPath": "static/tab/profile.png", "selectedIconPath": "static/tab/profile_active.png" }
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tab/home.png",
"selectedIconPath": "static/tab/home_active.png"
},
{
"pagePath": "pages/shop/index",
"text": "商城",
"iconPath": "static/tab/shop.png",
"selectedIconPath": "static/tab/shop_active.png"
},
{
"pagePath": "pages/cabinet/index",
"text": "货柜",
"iconPath": "static/tab/box.png",
"selectedIconPath": "static/tab/box_active.png"
},
{
"pagePath": "pages/mine/index",
"text": "我的",
"iconPath": "static/tab/profile.png",
"selectedIconPath": "static/tab/profile_active.png"
}
]
},
"globalStyle": {
@ -131,4 +154,4 @@
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}
}

View File

@ -23,51 +23,60 @@
</view>
<view v-show="tabActive === 'pool'">
<view class="rewards" v-if="currentIssueId && rewardsMap[currentIssueId] && rewardsMap[currentIssueId].length">
<el-card v-for="rw in rewardsMap[currentIssueId]" :key="rw.id" class="el-reward-card" shadow="hover">
<template #header>
<view class="el-card-header">
<text class="el-card-title">{{ rw.title }}</text>
<text v-if="rw.boss" class="badge-boss">BOSS</text>
</view>
</template>
<view class="card-image-wrap">
<image v-if="rw.image" class="card-image" :src="rw.image" mode="widthFix" />
<text class="prob-corner tl">概率 {{ rw.percent }}%</text>
<view class="rewards-grid" v-if="currentIssueId && rewardsMap[currentIssueId] && rewardsMap[currentIssueId].length">
<view v-for="(rw, idx) in rewardsMap[currentIssueId]" :key="rw.id"
class="reward-card animate-stagger"
:style="{ '--delay': idx * 0.05 + 's' }">
<view class="card-header">
<text class="card-title">{{ rw.title }}</text>
<text v-if="rw.boss" class="badge-boss">BOSS</text>
</view>
</el-card>
<view class="image-wrapper">
<image v-if="rw.image" class="reward-image" :src="rw.image" mode="aspectFill" />
<text class="prob-tag absolute-tag">概率 {{ rw.percent }}%</text>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-icon">📭</text>
<text class="empty-text">暂无奖励配置</text>
</view>
<view class="rewards-empty" v-else>暂无奖励配置</view>
</view>
<view v-show="tabActive === 'records'">
<view class="records" v-if="winRecords.length">
<el-card v-for="it in winRecords" :key="it.id" class="el-reward-card" shadow="hover">
<template #header>
<view class="el-card-header">
<text class="el-card-title">{{ it.title }}</text>
<text v-if="it.count !== undefined" class="badge-count">x{{ it.count }}</text>
<view class="records-list" v-if="winRecords.length">
<view v-for="(it, idx) in winRecords" :key="it.id"
class="record-item animate-stagger"
:style="{ '--delay': idx * 0.05 + 's' }">
<image class="record-img" :src="it.image" mode="aspectFill" />
<view class="record-info">
<view class="record-title">{{ it.title }}</view>
<view class="record-meta">
<text class="record-count">x{{ it.count }}</text>
<text v-if="it.percent !== undefined">占比 {{ it.percent }}%</text>
</view>
</template>
<view class="card-image-wrap">
<image v-if="it.image" class="card-image" :src="it.image" mode="widthFix" />
<text v-if="it.percent !== undefined" class="prob-corner tl">占比 {{ it.percent }}%</text>
</view>
</el-card>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-icon">📝</text>
<text class="empty-text">暂无中奖记录</text>
</view>
<view class="rewards-empty" v-else>暂无中奖记录</view>
</view>
</view>
<view v-else class="issues-empty">暂无期数</view>
</view>
</scroll-view>
<view class="float-actions">
<button class="float-btn primary" @click="onParticipate">立即参与</button>
<view class="float-bar">
<button class="action-btn primary" @click="onParticipate">
立即参与
<view class="btn-shine"></view>
</button>
</view>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { ref, computed } from 'vue'
import ElCard from '../../../components/ElCard.vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue, getActivityWinRecords } from '../../../api/appUser'
@ -241,21 +250,6 @@ function onIssueChange(e) {
currentIssueId.value = (cur && cur.id) || ''
}
async function ensureElCard() {
const inst = getCurrentInstance()
const app = inst && inst.appContext && inst.appContext.app
let comp = null
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
comp = window.ElementPlus.ElCard
} else {
comp = ElCard
}
if (app && !app.component('el-card')) app.component('el-card', comp)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
@ -292,40 +286,369 @@ onLoad((opts) => {
})
</script>
<style scoped>
.page { height: 100vh; padding-bottom: 140rpx }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
.float-actions { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: rgba(255,255,255,0.9); box-shadow: 0 -6rpx 16rpx rgba(0,0,0,0.06); z-index: 9999 }
.float-btn { width: 100%; border-radius: 999rpx }
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { }
.issue-picker { height: 200rpx; background: #f8f8f8; border-radius: 12rpx; margin-bottom: 64rpx }
.picker-item { height: 40rpx; line-height: 40rpx; text-align: center; font-size: 26rpx }
.tabs { display: flex; padding: 0 12rpx; margin-bottom: 16rpx }
.tab { flex: 1; text-align: center; font-size: 28rpx; padding: 16rpx 0; border: 2rpx solid #f0c58a; color: #8a5a2b; background: #fff3df; border-radius: 16rpx }
.tab + .tab { margin-left: 12rpx }
.tab.active { background: #ffdfaa; border-color: #ffb74d; color: #6b4b1f; font-weight: 600 }
.rewards { width: 100%; margin-top: 24rpx }
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
.el-reward-card { margin-bottom: 12rpx }
.el-card-header { display: flex; align-items: center; justify-content: space-between }
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
.card-image-wrap { position: relative; padding-bottom: 48rpx }
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
.prob-corner.tl { top: 12rpx; left: 12rpx }
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
.badge-count { background: #ffd166; color: #6b4b1f; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
.rewards-empty { font-size: 24rpx; color: #999 }
.issues-empty { font-size: 24rpx; color: #999 }
<style lang="scss" scoped>
/* ============================================
对对碰活动页面 - 高级设计重构 (SCSS Integration)
============================================ */
.page-container {
min-height: 100vh;
background: $bg-page;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80rpx);
opacity: 0.6;
}
.orb-1 {
width: 500rpx; height: 500rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.2) 0%, transparent 70%);
top: -100rpx; left: -100rpx;
}
.orb-2 {
width: 600rpx; height: 600rpx;
background: radial-gradient(circle, rgba($accent-gold, 0.15) 0%, transparent 70%);
bottom: -100rpx; right: -100rpx;
}
.page-content {
flex: 1;
position: relative;
z-index: 1;
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
/* Banner */
.banner-wrapper {
margin: $spacing-md $spacing-lg;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-lg;
position: relative;
animation: fadeInDown 0.6s ease-out;
}
.banner-img {
width: 100%;
display: block;
}
.banner-shadow {
position: absolute;
bottom: 0; left: 0; width: 100%; height: 40%;
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent);
}
/* Header */
.header-section {
padding: 0 $spacing-lg;
margin-bottom: $spacing-lg;
text-align: center;
animation: fadeIn 0.8s ease-out;
}
.title-row {
margin-bottom: $spacing-sm;
}
.title-text {
font-size: $font-xxl;
font-weight: 900;
background: $gradient-brand;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
.price-tag {
display: inline-flex;
align-items: baseline;
background: rgba($bg-card, 0.6);
padding: $spacing-xs $spacing-lg;
border-radius: $radius-round;
backdrop-filter: blur(20rpx);
box-shadow: $shadow-sm;
}
.price-label { font-size: $font-sm; color: $text-sub; margin-right: $spacing-xs; }
.price-symbol { font-size: $font-sm; color: $brand-primary; font-weight: 700; }
.price-value { font-size: $font-xl; color: $brand-primary; font-weight: 900; font-family: 'DIN Alternate', sans-serif; }
/* Glass Card */
.glass-card {
margin: 0 $spacing-lg $spacing-lg;
background: rgba($bg-card, 0.8);
backdrop-filter: blur(40rpx);
border-radius: $radius-xl;
padding: $spacing-lg;
box-shadow: $shadow-card;
border: 1rpx solid rgba(255, 255, 255, 0.6);
animation: fadeInUp 0.6s ease-out 0.2s backwards;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $spacing-md;
padding: 0 4rpx;
}
.section-title {
font-size: $font-lg;
font-weight: 800;
color: $text-main;
position: relative;
padding-left: 20rpx;
&::before {
content: '';
position: absolute;
left: 0; top: 50%; transform: translateY(-50%);
width: 8rpx; height: 32rpx;
background: $gradient-brand;
border-radius: 4rpx;
}
}
.issue-indicator {
font-size: $font-sm;
color: $brand-primary;
background: rgba($brand-primary, 0.1);
padding: 4rpx $spacing-md;
border-radius: $radius-round;
font-weight: 600;
}
/* Custom Picker */
.custom-picker {
height: 280rpx;
background: rgba($bg-secondary, 0.5);
border-radius: $radius-lg;
margin-bottom: $spacing-lg;
overflow: hidden;
}
.picker-item {
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-md;
}
.picker-text { font-size: $font-lg; color: $text-main; font-weight: 600; }
.picker-status {
font-size: $font-xs; color: $text-sub; background: rgba(0,0,0,0.05); padding: 2rpx $spacing-sm; border-radius: $radius-sm;
&.status-active { background: #D1FAE5; color: #059669; }
}
/* Modern Tabs */
.modern-tabs {
display: flex;
background: $bg-secondary;
padding: 8rpx;
border-radius: $radius-lg;
margin-bottom: $spacing-lg;
}
.tab-item {
flex: 1;
text-align: center;
padding: $spacing-md 0;
font-size: $font-md;
color: $text-sub;
border-radius: $radius-md;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
&.active {
background: #FFFFFF;
color: $brand-primary;
box-shadow: $shadow-sm;
}
}
.active-dot {
width: 8rpx; height: 8rpx;
background: $brand-primary;
border-radius: 50%;
position: absolute;
bottom: 8rpx; left: 50%; transform: translateX(-50%);
}
/* Rewards Grid */
.rewards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $spacing-lg;
}
.reward-card {
background: #FFFFFF;
border-radius: $radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.03);
display: flex;
flex-direction: column;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $spacing-md;
height: 44rpx;
}
.card-title {
font-size: $font-md;
color: $text-main;
font-weight: 600;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 8rpx;
}
.badge-boss {
font-size: $font-xs;
background: $gradient-gold;
color: #78350F;
padding: 2rpx $spacing-sm;
border-radius: $radius-sm;
font-weight: 800;
flex-shrink: 0;
}
.card-body {
flex: 1;
display: flex;
flex-direction: column;
}
.image-wrapper {
width: 100%;
padding-bottom: 100%;
position: relative;
background: $bg-secondary;
border-radius: $radius-md;
overflow: hidden;
margin-bottom: $spacing-sm;
}
.reward-image {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
}
.prob-tag {
position: absolute;
top: 8rpx; left: 8rpx;
font-size: $font-xs;
color: #fff;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4rpx);
padding: 4rpx $spacing-sm;
border-radius: $radius-sm;
z-index: 2;
}
/* Records List */
.records-list {
display: flex;
flex-direction: column;
gap: $spacing-lg;
}
.record-item {
display: flex;
background: #FFFFFF;
padding: $spacing-lg;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
align-items: center;
}
.record-img {
width: 100rpx; height: 100rpx;
border-radius: $radius-md;
background: $bg-secondary;
margin-right: $spacing-lg;
}
.record-info {
flex: 1;
}
.record-title {
font-size: $font-md;
font-weight: 600;
color: $text-main;
margin-bottom: $spacing-xs;
}
.record-meta {
display: flex;
gap: $spacing-md;
font-size: $font-sm;
color: $text-sub;
}
.record-count {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
padding: 2rpx $spacing-sm;
border-radius: $radius-sm;
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
color: $text-placeholder;
}
.empty-icon { font-size: 80rpx; margin-bottom: $spacing-lg; opacity: 0.5; }
.empty-text { font-size: $font-md; }
/* Float Bar */
.float-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
padding: $spacing-lg $spacing-xl;
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
box-shadow: 0 -8rpx 30rpx rgba(0, 0, 0, 0.05);
z-index: 100;
animation: slideUp 0.4s ease-out backwards;
}
.action-btn {
height: 96rpx;
border-radius: $radius-round;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-xl;
font-weight: 800;
position: relative;
overflow: hidden;
transition: all 0.2s;
&.primary {
background: $gradient-brand;
color: #fff;
box-shadow: $shadow-warm;
}
&:active { transform: scale(0.98); }
}
.btn-shine {
position: absolute;
top: 0; left: -100%; width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transform: skewX(-20deg);
animation: shine 3s infinite;
}
/* Animation Utilities */
.animate-stagger {
animation: fadeInUp 0.5s ease-out backwards;
animation-delay: var(--delay, 0s);
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<view class="page">
<scroll-view class="content" scroll-y>
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
<view v-else-if="filteredActivities.length > 0" class="activity-grid">
<view class="activity-item" v-for="a in filteredActivities" :key="a.id" @tap="onActivityTap(a)">
<view class="thumb-box">
<image class="thumb" :src="a.image" mode="aspectFill" />
<view class="tag-hot">HOT</view>
</view>
<view class="info">
<view class="name">{{ a.title }}</view>
<view class="bottom-row">
<text class="price-text">{{ a.category_name }} · {{ a.subtitle }}</text>
<view class="btn-go">GO</view>
</view>
</view>
</view>
</view>
<view v-else class="empty">
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
<text class="empty-text">暂无{{ title }}活动</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { request, authRequest } from '@/utils/request.js'
const title = ref('')
const categoryTarget = ref('')
const activities = ref([])
const loading = ref(false)
const filteredActivities = computed(() => {
if (!categoryTarget.value) return activities.value
const target = categoryTarget.value.trim()
return activities.value.filter(a => {
const cat = (a.category_name || '').trim()
return cat === target || cat.includes(target)
})
})
function apiGet(url) {
const token = uni.getStorageSync('token')
const fn = token ? authRequest : request
return fn({ url })
}
function cleanUrl(u) {
const s = String(u || '').trim()
const m = s.match(/https?:\/\/[^\s'"`]+/)
if (m && m[0]) return m[0]
return s.replace(/[`'\"]/g, '').trim()
}
function buildSubtitle(i) {
const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? ''
if (base) return base
const price = (i.price_draw !== undefined && i.price_draw !== null) ? `¥${(Number(i.price_draw || 0) / 100).toFixed(2)}` : ''
return price
}
async function loadData() {
loading.value = true
try {
const res = await apiGet('/api/app/activities')
let list = []
if (Array.isArray(res)) list = res
else if (res && (Array.isArray(res.list) || Array.isArray(res.data))) list = res.list || res.data
activities.value = list.map((i, idx) => ({
id: i.id ?? String(idx),
image: cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
title: (i.title ?? i.name ?? '').replace(/无限赏|一番赏|对对碰|爬塔/g, '').trim(),
subtitle: buildSubtitle(i),
category_name: i.category_name ?? i.categoryName ?? '',
link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '')
})).filter(i => i.image || i.title)
} catch (e) {
activities.value = []
} finally {
loading.value = false
}
}
function onActivityTap(a) {
const name = (a.category_name || '').trim()
const id = a.id
let path = ''
// Navigate to DETAIL, not list
if (name.includes('一番赏')) path = '/pages/activity/yifanshang/index'
else if (name.includes('无限赏')) path = '/pages/activity/wuxianshang/index'
else if (name.includes('对对碰')) path = '/pages/activity/duiduipeng/index'
else if (name.includes('爬塔')) path = '/pages/activity/pata/index'
if (path && id) {
uni.navigateTo({ url: `${path}?id=${id}` })
return
}
if (a.link && /^\/.+/.test(a.link)) {
uni.navigateTo({ url: a.link })
}
}
onLoad((opts) => {
if (opts && opts.category) {
categoryTarget.value = decodeURIComponent(opts.category)
title.value = categoryTarget.value
uni.setNavigationBarTitle({ title: categoryTarget.value })
}
loadData()
})
//
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
onShareAppMessage(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: `${title.value || '精彩活动'} - 奇盒潮玩`,
path: `/pages/index/index?invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
})
onShareTimeline(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: `${title.value || '精彩活动'} - 奇盒潮玩`,
query: `invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
})
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: $bg-page;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
padding: $spacing-lg;
}
.activity-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $spacing-md;
}
.activity-item {
background: #fff;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-sm;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
&:active {
transform: scale(0.98);
box-shadow: $shadow-xs;
}
}
.thumb-box {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
height: 0;
background: $bg-secondary;
}
.thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
background: linear-gradient(90deg, $bg-secondary 25%, #e8e8e8 50%, $bg-secondary 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.tag-hot {
position: absolute;
top: 16rpx; left: 16rpx;
background: rgba(255, 69, 58, 0.9);
color: #fff;
font-size: 20rpx;
padding: 6rpx 14rpx;
border-radius: 8rpx;
font-weight: 800;
letter-spacing: 1rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 69, 58, 0.3);
backdrop-filter: blur(4rpx);
}
.info {
padding: 20rpx 20rpx;
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
.name {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
height: 80rpx;
}
.bottom-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.price-text {
font-size: 22rpx;
color: $text-secondary;
max-width: 70%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn-go {
background: $brand-primary;
color: #fff;
font-size: 20rpx;
font-weight: 900;
padding: 6rpx 20rpx;
border-radius: 100rpx;
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.9);
background: darken($brand-primary, 5%);
}
}
.loading-wrap {
display: flex; justify-content: center; padding: 100rpx;
}
.spinner {
width: 48rpx; height: 48rpx;
border: 4rpx solid $border-color-light;
border-top-color: $brand-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
animation: fadeInUp 0.5s ease-out;
}
.empty-img {
width: 240rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
color: $text-secondary;
font-size: 28rpx;
}
/* ============================================
动画增强
============================================ */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* 卡片交错入场 */
@for $i from 1 through 10 {
.activity-item:nth-child(#{$i}) {
animation: fadeInUp 0.4s ease-out #{$i * 0.05}s both;
}
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -0,0 +1,573 @@
<template>
<view class="page-wrapper">
<!-- Rebuild Trigger -->
<!-- 背景层 -->
<image class="bg-fixed" :src="detail.banner || ''" mode="aspectFill" />
<view class="bg-mask"></view>
<view class="content-area">
<!-- 顶部信息 -->
<view class="header-section">
<view class="title-box">
<text class="main-title">{{ detail.name || detail.title || '爬塔挑战' }}</text>
<text class="sub-title">层层突围 赢取大奖</text>
</view>
<view class="rule-btn" @tap="showRules">规则</view>
</view>
<!-- 挑战区域 (模拟塔层) -->
<view class="tower-container">
<view class="tower-level current">
<view class="level-info">
<text class="level-num">当前挑战</text>
<text class="level-name">{{ currentIssueTitle || '第1层' }}</text>
</view>
<view class="level-status">进行中</view>
</view>
<!-- 奖池预览 -->
<view class="rewards-preview" v-if="currentIssueRewards.length">
<scroll-view scroll-x class="rewards-scroll">
<view class="reward-item" v-for="(r, idx) in currentIssueRewards" :key="idx">
<image class="reward-img" :src="r.image" mode="aspectFill" />
<view class="reward-name">{{ r.title }}</view>
<view class="reward-prob" v-if="r.percent">概率 {{ r.percent }}%</view>
</view>
</scroll-view>
</view>
</view>
<!-- 操作区 -->
<view class="action-area">
<view class="price-display">
<text class="currency">¥</text>
<text class="amount">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
<text class="unit">/</text>
</view>
<button class="challenge-btn" :loading="drawLoading" @tap="onStartChallenge">
立即挑战
</button>
</view>
</view>
<!-- 结果弹窗 -->
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
<view class="flip-mask" @tap="closeFlip"></view>
<view class="flip-content">
<FlipGrid ref="flipRef" :rewards="winItems" :controls="false" />
<button class="close-btn" @tap="closeFlip">收下奖励</button>
</view>
</view>
<PaymentPopup
v-model:visible="paymentVisible"
:amount="paymentAmount"
:coupons="coupons"
:propCards="propCards"
@confirm="onPaymentConfirm"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import FlipGrid from '../../../components/FlipGrid.vue'
import PaymentPopup from '../../../components/PaymentPopup.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '../../../api/appUser'
const activityId = ref('')
const detail = ref({})
const issues = ref([])
const currentIssueId = ref('')
const rewardsMap = ref({})
const drawLoading = ref(false)
const showFlip = ref(false)
const winItems = ref([])
const flipRef = ref(null)
// Payment
const paymentVisible = ref(false)
const paymentAmount = ref('0.00')
const coupons = ref([])
const propCards = ref([])
const selectedCoupon = ref(null)
const selectedCard = ref(null)
const pendingCount = ref(1)
const currentIssueTitle = computed(() => {
const i = issues.value.find(x => x.id === currentIssueId.value)
return i ? (i.title || `${i.no}`) : ''
})
const currentIssueRewards = computed(() => {
return (currentIssueId.value && rewardsMap.value[currentIssueId.value]) || []
})
const priceVal = computed(() => Number(detail.value.price_draw || 0) / 100)
async function loadData(id) {
try {
const d = await getActivityDetail(id)
detail.value = d || {}
const is = await getActivityIssues(id)
issues.value = normalizeIssues(is)
if (issues.value.length) {
const first = issues.value[0]
currentIssueId.value = first.id
loadRewards(id, first.id)
}
} catch (e) {
console.error(e)
}
}
async function loadRewards(aid, iid) {
try {
const res = await getActivityIssueRewards(aid, iid)
rewardsMap.value[iid] = normalizeRewards(res)
} catch (e) {}
}
function onStartChallenge() {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showToast({ title: '请先登录', icon: 'none' })
// In real app, redirect to login
return
}
if (!currentIssueId.value) {
uni.showToast({ title: '暂无挑战场次', icon: 'none' })
return
}
paymentAmount.value = priceVal.value.toFixed(2)
pendingCount.value = 1
paymentVisible.value = true
// Fetch coupons/cards in background
fetchPropCards()
fetchCoupons()
}
async function onPaymentConfirm(data) {
selectedCoupon.value = data?.coupon || null
selectedCard.value = data?.card || null
paymentVisible.value = false
await doDraw()
}
async function doDraw() {
drawLoading.value = true
try {
const openid = uni.getStorageSync('openid')
const joinRes = await joinLottery({
activity_id: Number(activityId.value),
issue_id: Number(currentIssueId.value),
channel: 'miniapp',
count: 1,
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0
})
if (!joinRes) throw new Error('下单失败')
const orderNo = joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no
// Simulate Wechat Pay flow (simplified)
const payRes = await createWechatOrder({ openid, order_no: orderNo })
await new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
...payRes,
success: resolve,
fail: reject
})
})
// Get Result
const res = await getLotteryResult(orderNo)
const raw = res.list || res.items || res.data || res.result || (Array.isArray(res) ? res : [res])
winItems.value = raw.map(i => ({
title: i.title || i.name || '未知奖励',
image: i.image || i.img || ''
}))
showFlip.value = true
setTimeout(() => {
if(flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(winItems.value)
}, 100)
} catch (e) {
uni.showToast({ title: e.message || '挑战失败', icon: 'none' })
} finally {
drawLoading.value = false
}
}
function normalizeIssues(list) {
if (!Array.isArray(list)) return []
return list.map(i => ({
id: i.id,
title: i.title || i.name,
no: i.no,
}))
}
function normalizeRewards(list) {
if (!Array.isArray(list)) return []
return list.map(i => ({
title: i.name || i.title,
image: i.image || i.img || i.pic,
percent: i.percent || 0
}))
}
async function fetchPropCards() { /* implementation same as other pages */ }
async function fetchCoupons() { /* implementation same as other pages */ }
function showRules() {
uni.showModal({ title: '规则', content: detail.value.rules || '暂无规则', showCancel: false })
}
function closeFlip() { showFlip.value = false }
onLoad((opts) => {
if (opts.id) {
activityId.value = opts.id
loadData(opts.id)
}
})
</script>
<style lang="scss" scoped>
/* ============================================
爬塔页面 - 沉浸式暗黑风格 (SCSS Integration)
============================================ */
$local-gold: #FFD700; //
.page-wrapper {
min-height: 100vh;
position: relative;
background: $bg-dark;
color: $text-dark-main;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 背景装饰 - 暗黑版 */
.bg-decoration {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
&::before {
content: '';
position: absolute;
top: -10%; left: -20%;
width: 600rpx; height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.1) 0%, transparent 70%);
filter: blur(80rpx);
border-radius: 50%;
opacity: 0.6;
animation: float 10s ease-in-out infinite;
}
&::after {
content: '';
position: absolute;
bottom: 10%; right: -10%;
width: 500rpx; height: 500rpx;
background: radial-gradient(circle, rgba($local-gold, 0.08) 0%, transparent 70%);
filter: blur(60rpx);
border-radius: 50%;
opacity: 0.5;
animation: float 12s ease-in-out infinite reverse;
}
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(20rpx, 30rpx); }
}
.bg-fixed {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
opacity: 0.3;
z-index: 0;
filter: blur(8rpx);
}
.bg-mask {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(180deg, rgba($bg-dark, 0.85), $bg-dark 95%);
z-index: 1;
}
.content-area {
position: relative;
z-index: 2;
flex: 1;
display: flex;
flex-direction: column;
padding: $spacing-lg;
padding-top: calc(env(safe-area-inset-top) + 20rpx);
}
.header-section {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $spacing-xl;
animation: fadeInDown 0.6s ease-out;
}
.title-box {
display: flex;
flex-direction: column;
}
.main-title {
font-size: 60rpx;
font-weight: 900;
font-style: italic;
display: block;
text-shadow: 0 4rpx 16rpx rgba(0,0,0,0.6);
background: linear-gradient(180deg, #fff, #b3b3b3);
-webkit-background-clip: text;
color: transparent;
letter-spacing: 2rpx;
}
.sub-title {
font-size: 26rpx;
opacity: 0.8;
margin-top: $spacing-xs;
display: block;
letter-spacing: 4rpx;
color: $brand-primary;
text-transform: uppercase;
}
.rule-btn {
background: rgba(255,255,255,0.1);
border: 1px solid $border-dark;
padding: 12rpx 32rpx;
border-radius: 100rpx;
font-size: 24rpx;
backdrop-filter: blur(10rpx);
transition: all 0.2s;
color: rgba(255,255,255,0.9);
&:active {
background: rgba(255,255,255,0.25);
transform: scale(0.96);
}
}
.tower-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-bottom: 40rpx;
}
.tower-level {
width: 100%;
background: $bg-dark-card;
backdrop-filter: blur(20rpx);
padding: 48rpx;
border-radius: $radius-xl;
box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.3);
margin-bottom: 40rpx;
border: 1px solid $border-dark;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
animation: zoomIn 0.5s ease-out backwards;
&::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
}
&.current {
background: rgba($local-gold, 0.15);
border-color: rgba($local-gold, 0.5);
box-shadow: 0 0 40rpx rgba($local-gold, 0.15), inset 0 0 20rpx rgba($local-gold, 0.05);
}
}
.level-info { display: flex; flex-direction: column; z-index: 1; }
.level-num {
font-size: 24rpx;
color: $text-dark-sub;
margin-bottom: 8rpx;
text-transform: uppercase;
letter-spacing: 2rpx;
}
.level-name {
font-size: 48rpx;
font-weight: 700;
color: $text-dark-main;
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.3);
}
.level-status {
font-size: 24rpx;
background: linear-gradient(135deg, $local-gold, $brand-secondary);
color: #3e2723;
padding: 8rpx 20rpx;
border-radius: 12rpx;
font-weight: 800;
box-shadow: 0 4rpx 16rpx rgba($brand-secondary, 0.3);
z-index: 1;
}
.rewards-preview {
width: 100%;
margin-top: 40rpx;
}
.rewards-scroll {
white-space: nowrap;
width: 100%;
}
.reward-item {
display: inline-flex;
flex-direction: column;
align-items: center;
width: 160rpx;
margin-right: 24rpx;
animation: fadeInUp 0.5s ease-out backwards;
@for $i from 1 through 5 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.1}s;
}
}
}
.reward-img {
width: 120rpx; height: 120rpx;
border-radius: 24rpx;
background: rgba(255,255,255,0.05);
margin-bottom: 16rpx;
border: 1px solid $border-dark;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
}
.reward-name {
font-size: 22rpx;
color: $text-dark-sub;
width: 100%;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.reward-prob {
font-size: 20rpx;
color: $local-gold;
font-weight: 600;
margin-top: 4rpx;
}
.action-area {
background: $bg-dark-card;
backdrop-filter: blur(40rpx);
padding: 24rpx 32rpx;
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid $border-dark;
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.5);
margin-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
animation: slideUp 0.6s ease-out backwards;
animation-delay: 0.3s;
}
.price-display {
display: flex;
align-items: baseline;
color: $local-gold;
font-weight: 700;
margin-left: 20rpx;
text-shadow: 0 0 20rpx rgba(255, 215, 0, 0.2);
}
.currency { font-size: 28rpx; }
.amount { font-size: 48rpx; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
.unit { font-size: 24rpx; opacity: 0.8; font-weight: normal; }
.challenge-btn {
background: $gradient-brand;
color: #fff;
font-weight: 900;
border-radius: 100rpx;
padding: 0 60rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 0, 0.3);
border: none;
position: relative;
overflow: hidden;
transition: all 0.2s;
&::after {
content: '';
position: absolute;
top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 3s infinite;
}
&:active {
transform: scale(0.96);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
}
}
.flip-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999;
}
.flip-mask {
position: absolute; top: 0; bottom: 0; width: 100%; background: rgba(0,0,0,0.85);
backdrop-filter: blur(10rpx);
animation: fadeIn 0.3s ease-out;
}
.flip-content {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
padding: 40rpx;
justify-content: center;
animation: zoomIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.close-btn {
margin-top: 60rpx;
background: #fff;
color: #333;
border-radius: 100rpx;
font-weight: 700;
width: 50%;
height: 80rpx;
line-height: 80rpx;
align-self: center;
box-shadow: 0 10rpx 30rpx rgba(255,255,255,0.15);
transition: all 0.2s;
&:active {
transform: scale(0.95);
}
}
@keyframes shimmer {
0% { left: -100%; }
50%, 100% { left: 200%; }
}
</style>

View File

@ -1,25 +1,63 @@
<template>
<view class="bg-decoration"></view>
<scroll-view class="page" scroll-y>
<!-- 顶部 Banner -->
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
</view>
<view class="draw-actions">
<button class="draw-btn" @click="() => openPayment(1)">参加一次</button>
<button class="draw-btn" @click="() => openPayment(10)">参加十次</button>
<button class="draw-btn secondary" @click="onMachineTry">试一试</button>
</view>
<view class="issues" v-if="showIssues && issues.length">
<view class="issue-switch">
<button class="switch-btn" @click="prevIssue"></button>
<text class="issue-title">{{ currentIssueTitle }}</text>
<button class="switch-btn" @click="nextIssue"></button>
<!-- 商品信息卡片 -->
<view class="product-card">
<view class="product-info">
<image v-if="detail.banner" class="product-thumb" :src="detail.banner" mode="aspectFill" />
<view class="product-detail">
<view class="product-name">{{ detail.name || detail.title || '无限赏活动' }}</view>
<view class="product-price">¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
</view>
<view class="product-actions">
<view class="action-btn">📦 盒柜</view>
</view>
</view>
</view>
<!-- 期号切换条 -->
<view class="issue-bar" v-if="showIssues && issues.length">
<button class="nav-btn" @click="prevIssue"></button>
<view class="issue-info">
<text class="issue-label">{{ currentIssueTitle }}</text>
</view>
<button class="nav-btn" @click="nextIssue"></button>
</view>
<!-- 玩法福利标签 -->
<view class="gameplay-tags">
<view class="tag tag-pool">聚宝盆</view>
<view class="tag tag-drop">随机掉落 10%</view>
<view class="tag tag-free">随机免单 10%</view>
</view>
</scroll-view>
<!-- 底部多档位抽赏按钮 -->
<view class="bottom-actions">
<button class="tier-btn" @click="() => openPayment(1)">
<text class="tier-price">¥{{ (pricePerDrawYuan * 1).toFixed(2) }}</text>
<text class="tier-label">抽1发</text>
</button>
<button class="tier-btn" @click="() => openPayment(3)">
<text class="tier-price">¥{{ (pricePerDrawYuan * 3).toFixed(2) }}</text>
<text class="tier-label">抽3发</text>
</button>
<button class="tier-btn" @click="() => openPayment(5)">
<text class="tier-price">¥{{ (pricePerDrawYuan * 5).toFixed(2) }}</text>
<text class="tier-label">抽5发</text>
</button>
<button class="tier-btn tier-hot" @click="() => openPayment(10)">
<text class="tier-price">¥{{ (pricePerDrawYuan * 10).toFixed(2) }}</text>
<text class="tier-label">抽10发</text>
</button>
</view>
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
<view class="flip-mask" @tap="closeFlip"></view>
<view class="flip-content" @tap.stop>
@ -38,8 +76,7 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import ElCard from '../../../components/ElCard.vue'
import { ref, computed } from 'vue'
import FlipGrid from '../../../components/FlipGrid.vue'
import { onLoad } from '@dcloudio/uni-app'
import PaymentPopup from '../../../components/PaymentPopup.vue'
@ -218,19 +255,6 @@ function nextIssue() {
currentIssueId.value = (cur && cur.id) || ''
}
async function ensureElCard() {
const inst = getCurrentInstance()
const app = inst && inst.appContext && inst.appContext.app
let comp = null
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
comp = window.ElementPlus.ElCard
} else {
comp = ElCard
}
if (app && !app.component('el-card')) app.component('el-card', comp)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
@ -381,49 +405,358 @@ onLoad((opts) => {
fetchDetail(id)
fetchIssues(id)
}
ensureElCard()
})
function closeFlip() { showFlip.value = false }
</script>
<style scoped>
.page { height: 100vh; padding-bottom: 140rpx }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
.draw-actions { display: flex; gap: 12rpx; padding: 24rpx }
.draw-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
.draw-btn.secondary { background: #ffd166; color: #6b4b1f }
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000 }
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1 }
.flip-content { position: relative; display: flex; flex-direction: column; height: 100%; padding: 24rpx; z-index: 2 }
.overlay-close { background: #ffd166; color: #6b4b1f; border-radius: 999rpx; align-self: flex-end }
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { }
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
.switch-btn { width: 72rpx; height: 72rpx; border-radius: 999rpx; background: #fff3df; border: 2rpx solid #f0c58a; color: #8a5a2b }
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
.rewards { width: 100%; margin-top: 24rpx }
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
.el-reward-card { margin-bottom: 12rpx }
.el-card-header { display: flex; align-items: center; justify-content: space-between }
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
.card-image-wrap { position: relative; padding-bottom: 48rpx }
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
.prob-corner.tl { top: 12rpx; left: 12rpx }
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
.rewards-empty { font-size: 24rpx; color: #999 }
.issues-empty { font-size: 24rpx; color: #999 }
<style lang="scss" scoped>
/* 奇盒潮玩 - 无限赏活动页面 */
.page {
min-height: 100vh;
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
background: transparent;
position: relative;
z-index: 1;
}
.bg-decoration {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: $bg-page;
z-index: 0;
overflow: hidden;
pointer-events: none;
&::before, &::after {
content: '';
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.5;
}
&::before {
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.12), transparent 70%);
top: -200rpx;
left: -200rpx;
animation: float 10s ease-in-out infinite;
}
&::after {
width: 500rpx;
height: 500rpx;
background: radial-gradient(circle, rgba($accent-gold, 0.15), transparent 70%);
bottom: 10%;
right: -100rpx;
animation: float 12s ease-in-out infinite reverse;
}
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 50rpx); }
}
.banner {
padding: $spacing-lg $spacing-lg 0;
animation: fadeInDown 0.6s $ease-out;
}
.banner-img {
width: 100%;
border-radius: $radius-lg;
box-shadow: $shadow-lg;
}
/* 商品信息卡片 */
.product-card {
margin: $spacing-lg;
background: $bg-glass;
backdrop-filter: blur(20rpx);
border-radius: $radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-card;
animation: fadeInUp 0.6s $ease-out 0.1s backwards;
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.product-info {
display: flex;
align-items: flex-start;
gap: $spacing-lg;
}
.product-thumb {
width: 140rpx;
height: 140rpx;
border-radius: $radius-md;
flex-shrink: 0;
background: $bg-page;
box-shadow: $shadow-inner;
}
.product-detail {
flex: 1;
min-width: 0;
}
.product-name {
font-size: $font-lg;
font-weight: 700;
color: $text-main;
margin-bottom: $spacing-sm;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.product-price {
font-size: $font-xl;
font-weight: 800;
color: $brand-primary;
font-family: 'DIN Alternate', sans-serif;
}
.product-actions {
display: flex;
flex-direction: column;
gap: $spacing-sm;
}
.action-btn {
background: rgba($brand-primary, 0.05);
border: 1rpx solid rgba($brand-primary, 0.2);
border-radius: $radius-sm;
padding: $spacing-sm $spacing-lg;
font-size: $font-sm;
color: $brand-primary-dark;
text-align: center;
font-weight: 600;
transition: all $transition-fast;
}
.action-btn:active {
background: rgba($brand-primary, 0.1);
transform: scale(0.95);
}
/* 期号切换条 */
.issue-bar {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-lg;
margin: 0 $spacing-lg $spacing-lg;
padding: $spacing-md $spacing-lg;
background: $bg-glass;
backdrop-filter: blur(20rpx);
border-radius: $radius-round;
box-shadow: $shadow-sm;
animation: fadeInUp 0.6s $ease-out 0.2s backwards;
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.nav-btn {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: $bg-page;
color: $text-sub;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-sm;
padding: 0;
margin: 0;
line-height: 1;
transition: all $transition-fast;
border: none;
&:active {
background: darken($bg-page, 5%);
transform: scale(0.9);
}
}
.issue-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
min-width: 200rpx;
}
.issue-label {
font-size: $font-lg;
font-weight: 700;
color: $text-main;
}
/* 玩法福利标签 */
.gameplay-tags {
display: flex;
gap: $spacing-md;
padding: 0 $spacing-lg;
margin-bottom: $spacing-lg;
flex-wrap: wrap;
animation: fadeInUp 0.6s $ease-out 0.3s backwards;
}
.tag {
padding: $spacing-sm $spacing-lg;
border-radius: $radius-round;
font-size: $font-sm;
font-weight: 600;
display: flex;
align-items: center;
box-shadow: $shadow-sm;
backdrop-filter: blur(4px);
}
.tag-pool {
background: $color-success;
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba($color-success, 0.3);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
.tag-drop {
background: $gradient-brand;
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.3);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
.tag-free {
background: $gradient-gold;
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba($accent-gold, 0.3);
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.1);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
/* 底部多档位抽赏按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: $spacing-md;
padding: $spacing-lg $spacing-lg;
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20rpx);
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.08);
z-index: 999;
animation: slideUp $transition-slow $ease-out backwards;
border-top: 1rpx solid rgba(0,0,0,0.05);
}
.tier-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-md $spacing-xs;
background: $bg-card;
border: 1rpx solid $border-color-light;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
transition: all $transition-fast;
&:active {
transform: scale(0.95);
background: $bg-page;
}
}
.tier-price {
font-size: $font-lg;
font-weight: 800;
color: $text-main;
font-family: 'DIN Alternate', sans-serif;
}
.tier-label {
font-size: $font-xs;
color: $text-sub;
margin-top: 4rpx;
font-weight: 500;
}
.tier-hot {
background: $gradient-brand;
border: none;
box-shadow: $shadow-warm;
position: relative;
overflow: hidden;
.tier-price, .tier-label {
color: #fff;
}
&::after {
content: 'HOT';
position: absolute;
top: 0;
right: 0;
background: linear-gradient(135deg, $accent-red, #D32F2F);
color: #fff;
font-size: 18rpx;
font-weight: 800;
padding: 4rpx 10rpx;
border-bottom-left-radius: $radius-md;
box-shadow: -2rpx 2rpx 4rpx rgba(0,0,0,0.1);
}
&:active {
opacity: 0.9;
transform: scale(0.96);
}
}
.tier-hot .tier-price, .tier-hot .tier-label {
color: #FFFFFF;
}
/* 翻牌弹窗 */
.flip-overlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 10000;
animation: fadeIn 0.3s ease-out;
}
.flip-mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(10px);
z-index: 1;
}
.flip-content {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
padding: 24rpx;
z-index: 2;
justify-content: center;
align-items: center;
animation: zoomIn 0.3s $ease-bounce;
}
.overlay-close {
margin-top: 60rpx;
width: 240rpx;
height: 88rpx;
line-height: 88rpx;
background: rgba(255,255,255,0.15) !important;
border: 1rpx solid rgba(255,255,255,0.3);
color: #FFFFFF !important;
border-radius: $radius-round;
font-weight: 600;
font-size: 30rpx;
backdrop-filter: blur(10px);
transition: all $transition-fast;
&:active {
background: rgba(255,255,255,0.25) !important;
transform: scale(0.95);
}
}
</style>

View File

@ -1,32 +1,99 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">抽选价{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
</view>
<view class="issues" v-if="showIssues && issues.length">
<view class="issue-switch">
<button class="switch-btn" @click="prevIssue"></button>
<text class="issue-title">{{ currentIssueTitle }}</text>
<button class="switch-btn" @click="nextIssue"></button>
</view>
</view>
<!-- 引入位置选择组件 -->
<view class="selector-section" v-if="activityId && currentIssueId">
<YifanSelector
:activity-id="activityId"
:issue-id="currentIssueId"
:price-per-draw="Number(detail.price_draw || 0) / 100"
@payment-success="onPaymentSuccess"
/>
<view class="page-wrapper">
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="orb orb-1"></view>
<view class="orb orb-2"></view>
</view>
</scroll-view>
<!-- 顶部背景图模糊处理 -->
<view class="page-bg">
<image class="bg-image" :src="detail.banner" mode="aspectFill" />
<view class="bg-mask"></view>
</view>
<!-- 导航栏占位如果有自定义导航栏需求 -->
<!-- <view class="nav-bar-placeholder"></view> -->
<!-- 主要内容区域 -->
<scroll-view class="main-scroll" scroll-y>
<!-- 头部信息卡片 -->
<view class="header-card animate-enter">
<image class="header-cover" :src="detail.banner" mode="aspectFill" />
<view class="header-info">
<view class="header-title">{{ detail.name || detail.title || '一番赏活动' }}</view>
<view class="header-price-row">
<text class="price-symbol">¥</text>
<text class="price-num">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
<text class="price-unit">/</text>
</view>
<view class="header-tags">
<view class="tag-item">超高爆率</view>
<view class="tag-item">公平公正</view>
</view>
</view>
<view class="header-actions">
<view class="action-btn" @tap="showRules">
<text class="icon">📋</text>
<text>规则</text>
</view>
<view class="action-btn" @tap="goCabinet">
<text class="icon">📦</text>
<text>盒柜</text>
</view>
</view>
</view>
<!-- 赏品概览 -->
<view class="section-container animate-enter stagger-1" v-if="currentIssueRewards.length > 0">
<view class="section-header">
<text class="section-title">赏品一览</text>
<text class="section-more">查看全部 ></text>
</view>
<scroll-view class="preview-scroll" scroll-x>
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="idx">
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : (item.grade || '赏') }}</view>
<image class="preview-img" :src="item.image" mode="aspectFill" />
<view class="preview-name">{{ item.title }}</view>
</view>
</scroll-view>
</view>
<!-- 选号区域 -->
<view class="section-container selector-container animate-enter stagger-2">
<!-- 期号切换 -->
<view class="issue-header">
<view class="issue-switch-btn" @click="prevIssue">
<text class="arrow"></text>
</view>
<view class="issue-info-center">
<text class="issue-current-text">{{ currentIssueTitle }}</text>
<text class="issue-status-badge">进行中</text>
</view>
<view class="issue-switch-btn" @click="nextIssue">
<text class="arrow"></text>
</view>
</view>
<!-- 选号组件 -->
<view class="selector-body" v-if="activityId && currentIssueId">
<YifanSelector
:activity-id="activityId"
:issue-id="currentIssueId"
:price-per-draw="Number(detail.price_draw || 0) / 100"
@payment-success="onPaymentSuccess"
/>
</view>
</view>
<!-- 底部垫高 -->
<view style="height: 180rpx;"></view>
</scroll-view>
</view>
<!-- 翻牌弹窗 -->
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
<view class="flip-mask" @tap="closeFlip"></view>
<view class="flip-content" @tap.stop>
@ -38,8 +105,7 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import ElCard from '../../../components/ElCard.vue'
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import FlipGrid from '../../../components/FlipGrid.vue'
import YifanSelector from '@/components/YifanSelector.vue'
@ -65,6 +131,26 @@ const currentIssueTitle = computed(() => {
const t = (cur && (cur.title || ('第' + (cur.no || '-') + '期'))) || '-'
return t
})
//
const currentIssueRemain = computed(() => {
const arr = issues.value || []
const cur = arr[selectedIssueIndex.value]
return cur && cur.remain !== undefined ? cur.remain : ''
})
//
function showRules() {
uni.showModal({
title: '活动规则',
content: detail.value.rules || '1. 选择号码进行抽选\n2. 每个号码对应一个奖品\n3. 已售号码不可再选',
showCancel: false
})
}
//
function goCabinet() {
uni.navigateTo({ url: '/pages/cabinet/index' })
}
function statusToText(s) {
if (s === 1) return '进行中'
@ -232,17 +318,7 @@ function nextIssue() {
currentIssueId.value = (cur && cur.id) || ''
}
async function ensureElCard() {
const inst = getCurrentInstance()
const app = inst && inst.appContext && inst.appContext.app
let comp = null
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
comp = window.ElementPlus.ElCard
} else {
comp = ElCard
}
if (app && !app.component('el-card')) app.component('el-card', comp)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
@ -292,61 +368,436 @@ onLoad((opts) => {
fetchIssues(id)
fetchWinRecords(id)
}
ensureElCard()
})
function closeFlip() { showFlip.value = false }
</script>
<style scoped>
.page { height: 100vh; padding-bottom: 140rpx }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
.draw-actions { display: flex; gap: 12rpx; padding: 24rpx }
.draw-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
.draw-btn.secondary { background: #ffd166; color: #6b4b1f }
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000 }
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1 }
.flip-content { position: relative; display: flex; flex-direction: column; height: 100%; padding: 24rpx; z-index: 2 }
.overlay-close { background: #ffd166; color: #6b4b1f; border-radius: 999rpx; align-self: flex-end }
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { }
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
.switch-btn {
width: 72rpx;
height: 72rpx;
border-radius: 999rpx;
background: #fff3df;
border: 2rpx solid #f0c58a;
color: #8a5a2b;
<style lang="scss" scoped>
/* ============================================
一番赏页面 - 高级设计重构 (SCSS Integration)
============================================ */
.page-wrapper {
min-height: 100vh;
background: $bg-page;
position: relative;
overflow: hidden;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.6;
}
.orb-1 {
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.2), transparent 70%);
top: -200rpx;
left: -200rpx;
animation: float 10s ease-in-out infinite;
}
.orb-2 {
width: 500rpx;
height: 500rpx;
background: radial-gradient(circle, rgba($accent-gold, 0.2), transparent 70%);
bottom: 20%;
right: -100rpx;
animation: float 12s ease-in-out infinite reverse;
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 50rpx); }
}
/* 顶部背景 */
.page-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 700rpx; /* 加高背景区域 */
z-index: 1;
}
.bg-image {
width: 100%;
height: 100%;
filter: blur(30rpx) brightness(0.9); /* 降低亮度提升文字对比度 */
transform: scale(1.1); /* 防止模糊边缘 */
}
.bg-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba($bg-page, 0.2) 0%, $bg-page 90%, $bg-page 100%);
}
.main-scroll {
position: relative;
z-index: 2;
height: 100vh;
}
/* 头部卡片 */
.header-card {
margin: $spacing-xl $spacing-lg;
background: rgba($bg-card, 0.85);
backdrop-filter: blur(24rpx);
border-radius: $radius-xl;
padding: $spacing-lg;
display: flex;
align-items: center;
box-shadow: $shadow-card;
border: 1rpx solid rgba(255, 255, 255, 0.6);
position: relative;
overflow: hidden;
/* 光泽效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
}
}
.header-cover {
width: 180rpx;
height: 180rpx;
border-radius: $radius-md;
margin-right: $spacing-lg;
background: $bg-secondary;
box-shadow: $shadow-md;
flex-shrink: 0;
}
.header-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
height: 180rpx;
}
.header-title {
font-size: $font-xl;
font-weight: 800;
color: $text-main;
margin-bottom: $spacing-xs;
line-height: 1.3;
@include text-ellipsis(2);
}
.header-price-row {
display: flex;
align-items: baseline;
color: $brand-primary;
margin-bottom: $spacing-sm;
text-shadow: 0 2rpx 4rpx rgba($brand-primary, 0.1);
}
.price-symbol { font-size: $font-md; font-weight: 700; }
.price-num { font-size: $font-xxl; font-weight: 900; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
.price-unit { font-size: $font-sm; color: $text-sub; margin-left: 4rpx; }
.header-tags {
display: flex;
gap: $spacing-xs;
flex-wrap: wrap;
}
.tag-item {
font-size: $font-xs;
color: $brand-primary-dark;
background: rgba($brand-primary, 0.08);
padding: 4rpx $spacing-sm;
border-radius: $radius-sm;
font-weight: 600;
border: 1rpx solid rgba($brand-primary, 0.1);
}
.header-actions {
display: flex;
flex-direction: column;
gap: $spacing-lg;
margin-left: 20rpx;
padding-left: $spacing-lg;
border-left: 1rpx solid rgba(0,0,0,0.06);
justify-content: center;
height: 140rpx;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: $font-xs;
color: $text-sub;
transition: all 0.2s;
&:active {
transform: scale(0.9);
color: $text-main;
}
}
.action-btn .icon {
font-size: $font-xl;
margin-bottom: 6rpx;
filter: grayscale(0.2);
}
/* 通用板块容器 */
.section-container {
margin: 0 $spacing-lg $spacing-lg;
background: rgba(255, 255, 255, 0.9); /* 略微透明 */
border-radius: $radius-xl;
padding: $spacing-lg;
box-shadow: $shadow-sm;
backdrop-filter: blur(10rpx);
}
/* 板块标题 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding: 0 4rpx;
}
.section-title {
font-size: $font-lg;
font-weight: 800;
color: $text-main;
position: relative;
padding-left: $spacing-lg;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 28rpx;
background: $gradient-brand;
border-radius: 4rpx;
}
}
.section-more {
font-size: $font-sm;
color: $text-tertiary;
display: flex;
align-items: center;
&::after {
content: '>';
font-family: monospace;
margin-left: 6rpx;
font-weight: 700;
}
}
/* 奖品概览 */
.preview-scroll {
white-space: nowrap;
margin: 0 -$spacing-lg; /* 负边距抵消padding */
padding: 0 $spacing-lg;
width: calc(100% + 40rpx);
}
.preview-item {
display: inline-block;
width: 200rpx;
margin-right: $spacing-lg;
vertical-align: top;
position: relative;
transition: transform 0.2s;
&:active {
transform: scale(0.96);
}
&:last-child {
margin-right: 40rpx;
}
}
.preview-img {
width: 200rpx;
height: 200rpx;
border-radius: $radius-lg;
background: $bg-secondary;
margin-bottom: $spacing-md;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.03);
}
.preview-name {
font-size: $font-sm;
color: $text-secondary;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
font-weight: 500;
}
.prize-tag {
position: absolute;
top: 10rpx;
left: 10rpx;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: $font-xs;
padding: 4rpx $spacing-sm;
border-radius: $radius-sm;
z-index: 10;
font-weight: 700;
backdrop-filter: blur(4rpx);
transform: scale(0.9);
transform-origin: top left;
}
.prize-tag.tag-boss {
background: $gradient-brand;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.4);
}
/* 选号区容器 */
.selector-container {
min-height: 800rpx;
display: flex;
flex-direction: column;
background: rgba($bg-card, 0.95);
backdrop-filter: blur(20rpx);
}
/* 期号头部 */
.issue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
background: $bg-grey;
border-radius: $radius-round; /* 胶囊形 */
padding: 10rpx;
border: 1rpx solid $border-color-light;
}
.issue-switch-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
background: $bg-card;
border-radius: 50%;
box-shadow: $shadow-sm;
transition: all 0.2s;
color: $text-secondary;
&:active {
transform: scale(0.9);
background: $bg-secondary;
color: $brand-primary;
}
}
.arrow {
font-size: $font-sm;
font-weight: 800;
}
.issue-info-center {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.issue-current-text {
font-size: $font-lg;
font-weight: 700;
color: $text-main;
}
.issue-status-badge {
font-size: $font-xs;
color: $uni-color-success;
background: rgba($uni-color-success, 0.1);
padding: 2rpx $spacing-md;
border-radius: $radius-round;
margin-top: 4rpx;
font-weight: 600;
}
.selector-body {
flex: 1;
}
/* 翻牌弹窗 */
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000; }
.flip-mask {
position: absolute; left: 0; right: 0; top: 0; bottom: 0;
background: rgba(0,0,0,0.75);
backdrop-filter: blur(10rpx);
z-index: 1;
animation: fadeIn 0.3s ease-out;
}
.flip-content {
position: relative; display: flex; flex-direction: column; height: 100%; padding: 40rpx; z-index: 2;
animation: scaleIn 0.3s ease-out;
}
.overlay-close {
background: rgba(255,255,255,0.2) !important;
color: #FFFFFF !important;
border-radius: 999rpx;
align-self: center;
margin-top: 40rpx;
font-weight: 600;
border: 1rpx solid rgba(255,255,255,0.3);
padding: 10rpx 60rpx;
font-size: 30rpx;
backdrop-filter: blur(10rpx);
&:active {
background: rgba(255,255,255,0.3) !important;
}
}
/* 动画定义 */
.animate-enter {
animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}
.stagger-1 { animation-delay: 0.1s; }
.stagger-2 { animation-delay: 0.2s; }
.stagger-3 { animation-delay: 0.3s; }
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
.el-reward-card { margin-bottom: 12rpx }
.el-card-header { display: flex; align-items: center; justify-content: space-between }
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
.card-image-wrap { position: relative; padding-bottom: 48rpx }
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
.prob-corner.tl { top: 12rpx; left: 12rpx }
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
.badge-count { background: #ffd166; color: #6b4b1f; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
.rewards-empty { font-size: 24rpx; color: #999 }
.issues-empty { font-size: 24rpx; color: #999 }
</style>

View File

@ -148,11 +148,87 @@ onLoad((opts) => {
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.form-item { display: flex; align-items: center; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 12rpx }
.label { width: 160rpx; font-size: 28rpx; color: #666 }
.input { flex: 1; font-size: 28rpx }
.submit { width: 100%; margin-top: 20rpx }
.error { color: #e43; margin-top: 12rpx }
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 地址编辑页面
采用暖橙色调的表单设计
============================================ */
.wrap {
padding: $spacing-lg;
min-height: 100vh;
background: $bg-page;
}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: $radius-lg;
padding: $spacing-lg $spacing-xl;
margin-bottom: $spacing-md;
box-shadow: $shadow-sm;
transition: all 0.2s;
&:focus-within {
box-shadow: $shadow-md;
transform: translateY(-2rpx);
}
}
.label {
width: 160rpx;
font-size: $font-md;
font-weight: 600;
color: $text-main;
flex-shrink: 0;
}
.input {
flex: 1;
font-size: $font-md;
color: $text-main;
background: transparent;
height: 48rpx;
}
/* 提交按钮 */
.submit {
width: 100%;
height: 96rpx;
line-height: 96rpx;
margin-top: 60rpx;
background: $gradient-brand !important;
color: #FFFFFF !important;
border-radius: $radius-round;
font-size: $font-lg;
font-weight: 800;
border: none;
box-shadow: $shadow-warm;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
box-shadow: none;
}
&[disabled] {
opacity: 0.6;
box-shadow: none;
background: $text-disabled !important;
}
}
/* 错误提示 */
.error {
color: $color-error;
font-size: $font-sm;
margin-top: $spacing-lg;
padding: $spacing-md;
background: rgba($color-error, 0.1);
border-radius: $radius-md;
text-align: center;
font-weight: 500;
}
</style>

View File

@ -113,18 +113,158 @@ onLoad(() => {
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.header { display: flex; justify-content: flex-end; margin-bottom: 12rpx }
.add { font-size: 28rpx }
.addr { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.addr-row { display: flex; align-items: center; margin-bottom: 8rpx }
.name { font-size: 30rpx; margin-right: 12rpx }
.phone { font-size: 28rpx; color: #666 }
.default { font-size: 24rpx; color: #007AFF; margin-left: 10rpx }
.region { font-size: 26rpx; color: #666 }
.detail { font-size: 26rpx; color: #333 }
.addr-actions { display: flex; justify-content: flex-end; gap: 12rpx; margin-top: 12rpx }
.empty { text-align: center; color: #999; margin-top: 40rpx }
.error { color: #e43; margin-bottom: 12rpx }
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 地址管理页面
采用暖橙色调的卡片列表设计
============================================ */
.wrap {
padding: $spacing-md;
min-height: 100vh;
background-color: $bg-page;
}
.header {
display: flex;
justify-content: flex-end;
margin-bottom: $spacing-lg;
}
.add {
font-size: $font-md;
background: $gradient-brand !important;
color: #FFFFFF !important;
border-radius: $radius-round;
padding: 0 $spacing-xl;
height: 72rpx;
line-height: 72rpx;
font-weight: 600;
box-shadow: $shadow-warm;
}
.add:active {
transform: scale(0.96);
}
/* 地址卡片 */
.addr {
background: #FFFFFF;
border-radius: $radius-md;
padding: $spacing-lg;
margin-bottom: $spacing-md;
box-shadow: $shadow-sm;
animation: fadeInUp 0.4s ease-out backwards;
}
@for $i from 1 through 10 {
.addr:nth-child(#{$i}) {
animation-delay: #{$i * 0.05}s;
}
}
.addr-main {
margin-bottom: $spacing-md;
}
.addr-row {
display: flex;
align-items: center;
margin-bottom: $spacing-sm;
}
.addr-row:last-child {
margin-bottom: 0;
}
.name {
font-size: $font-lg;
font-weight: 600;
color: $text-main;
}
.phone {
font-size: $font-md;
color: $text-sub;
}
.default {
font-size: $font-xs;
color: #FFFFFF;
background: $gradient-brand;
padding: 4rpx $spacing-sm;
border-radius: $radius-round;
font-weight: 500;
}
.region {
font-size: $font-sm;
color: $text-sub;
}
.detail {
font-size: $font-md;
color: $text-main;
line-height: 1.5;
}
/* 操作按钮 */
.addr-actions {
display: flex;
justify-content: flex-end;
gap: $spacing-md;
margin-top: $spacing-lg;
padding-top: $spacing-lg;
border-top: 1rpx solid $border-color-light;
}
.addr-actions button {
font-size: $font-sm;
height: 52rpx;
line-height: 52rpx;
padding: 0 $spacing-lg;
border-radius: $radius-round;
margin: 0;
font-weight: 600;
border: none;
background: $bg-secondary;
color: $text-main;
&::after { border: none; }
&:active {
transform: scale(0.96);
background: darken($bg-secondary, 5%);
}
}
.addr-actions button[type="warn"] {
background: rgba($color-error, 0.1) !important;
color: $color-error !important;
}
.addr-actions button:not([type]) {
background: $bg-secondary !important;
color: $text-main !important;
}
/* 空状态 */
.empty {
text-align: center;
color: $text-sub;
margin-top: 120rpx;
font-size: $font-md;
}
/* 错误提示 */
.error {
color: $color-error;
font-size: $font-sm;
margin-bottom: $spacing-md;
padding: $spacing-md;
background: rgba($color-error, 0.1);
border-radius: $radius-md;
text-align: center;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -62,20 +62,65 @@
<!-- Tab 1: 已申请发货 -->
<block v-if="currentTab === 1">
<view v-if="loading && shippedList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!shippedList || shippedList.length === 0" class="status-text">暂无发货记录</view>
<view v-else-if="!shippedList || shippedList.length === 0" class="status-text">暂无发货记录</view>
<view v-else class="inventory-grid">
<view v-for="(item, index) in shippedList" :key="index" class="inventory-item">
<!-- 已发货仅展示 -->
<image :src="item.image" mode="aspectFill" class="item-image" @error="onImageError(index, 'shipped')" />
<view class="item-info">
<text class="item-name">{{ item.name || '未命名道具' }}</text>
<text class="item-count">x{{ item.count || 1 }}</text>
<text class="item-status">已申请发货</text>
<text class="item-meta" v-if="item.express_code || item.express_no">快递{{ item.express_code }} {{ item.express_no }}</text>
<text class="item-meta" v-if="item.shipped_at">发货时间{{ formatDate(item.shipped_at) }}</text>
<text class="item-meta" v-if="item.received_at">签收时间{{ formatDate(item.received_at) }}</text>
</view>
<view v-else class="shipment-list">
<view v-for="(item, index) in shippedList" :key="index" class="shipment-card">
<!-- 头部批次信息和状态 -->
<view class="shipment-header">
<view class="shipment-batch">
<text class="batch-label">发货单</text>
<text class="batch-no" v-if="item.batch_no">{{ item.batch_no }}</text>
<view class="count-badge">{{ item.count }}件商品</view>
</view>
<view class="shipment-status" :class="getStatusClass(item.status)">
{{ getStatusText(item.status) }}
</view>
</view>
<!-- 商品缩略图列表 -->
<view class="product-thumbnails">
<view class="thumb-scroll">
<image
v-for="(img, imgIdx) in item.product_images.slice(0, 4)"
:key="imgIdx"
:src="img"
mode="aspectFill"
class="thumb-img"
@error="onThumbError(index, imgIdx)"
/>
<view class="thumb-more" v-if="item.product_images.length > 4">
+{{ item.product_images.length - 4 }}
</view>
</view>
<!-- 商品名称列表 -->
<view class="product-names">
<text class="product-name-item" v-for="(name, nIdx) in item.product_names.slice(0, 3)" :key="nIdx">
{{ name }}
</text>
<text class="product-name-more" v-if="item.product_names.length > 3">
{{ item.product_names.length }}件商品
</text>
</view>
</view>
<!-- 物流信息 -->
<view class="shipment-express" v-if="item.express_code || item.express_no">
<view class="express-icon">
<text class="iconfont">📦</text>
</view>
<view class="express-info">
<text class="express-company">{{ item.express_code || '待发货' }}</text>
<text class="express-no" v-if="item.express_no">{{ item.express_no }}</text>
</view>
<text class="express-copy" v-if="item.express_no" @tap="copyExpressNo(item.express_no)">复制</text>
</view>
<!-- 时间信息 -->
<view class="shipment-time">
<text class="time-item" v-if="item.created_at">申请时间{{ formatDate(item.created_at) }}</text>
<text class="time-item" v-if="item.shipped_at">发货时间{{ formatDate(item.shipped_at) }}</text>
</view>
</view>
</view>
<view v-if="loading && shippedList.length > 0" class="loading-more">加载更多...</view>
@ -217,6 +262,42 @@ function formatDate(dateStr) {
return `${y}-${m}-${da} ${h}:${min}`
}
//
function getStatusClass(status) {
const statusMap = {
1: 'status-pending', //
2: 'status-shipped', //
3: 'status-delivered', //
4: 'status-cancelled' //
}
return statusMap[status] || 'status-pending'
}
function getStatusText(status) {
const statusMap = {
1: '待发货',
2: '运输中',
3: '已签收',
4: '已取消'
}
return statusMap[status] || '待发货'
}
function copyExpressNo(expressNo) {
uni.setClipboardData({
data: expressNo,
success: () => {
uni.showToast({ title: '已复制单号', icon: 'success' })
}
})
}
function onThumbError(shipmentIndex, imgIndex) {
if (shippedList.value[shipmentIndex] && shippedList.value[shipmentIndex].product_images) {
shippedList.value[shipmentIndex].product_images[imgIndex] = '/static/logo.png'
}
}
async function loadShipments(uid) {
if (loading.value) return
loading.value = true
@ -227,21 +308,51 @@ async function loadShipments(uid) {
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
const mapped = list.map(s => ({
image: '/static/logo.png',
name: '发货单',
count: s.count ?? (Array.isArray(s.inventory_ids) ? s.inventory_ids.length : 0),
express_code: s.express_code || '',
express_no: s.express_no || '',
shipped_at: s.shipped_at || '',
received_at: s.received_at || '',
status: s.status
}))
// - 使 products
const mapped = list.map(s => {
const products = s.products || []
let productImages = []
let productNames = []
// products
products.forEach(product => {
if (product) {
// image JSON
const rawImg = product.image || product.images || product.main_image
const img = cleanUrl(rawImg)
productImages.push(img)
productNames.push(product.name || product.title || '商品')
}
})
// 使
if (productImages.length === 0) {
productImages = ['/static/logo.png']
productNames = ['未知商品']
}
return {
batch_no: s.batch_no || '',
count: s.count ?? (Array.isArray(s.inventory_ids) ? s.inventory_ids.length : 0),
product_ids: s.product_ids || [],
product_images: productImages,
product_names: productNames,
express_code: s.express_code || '',
express_no: s.express_no || '',
created_at: s.created_at || '',
shipped_at: s.shipped_at || '',
received_at: s.received_at || '',
status: s.status || 1
}
})
const next = page.value === 1 ? mapped : [...shippedList.value, ...mapped]
shippedList.value = next
if (list.length < pageSize.value || (page.value * pageSize.value >= total && total > 0)) { hasMore.value = false } else { page.value += 1 }
if (list.length === 0) { hasMore.value = false }
} catch (e) {
console.error('Load shipments error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
@ -569,246 +680,480 @@ async function onShip() {
}
</script>
<style scoped>
.item-status {
font-size: 24rpx;
color: #007AFF;
margin-top: 4rpx;
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 货柜页面
采用现代卡片式布局统一设计语言
============================================ */
.wrap {
padding: 0;
min-height: 100vh;
background: $bg-page;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
}
.item-meta { font-size: 22rpx; color: #666; margin-top: 4rpx }
.wrap { padding: 30rpx; }
/* 顶部 Tab */
.tabs {
display: flex;
background: #f5f5f5;
border-radius: 16rpx;
padding: 8rpx;
margin-bottom: 20rpx;
}
.action-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 88rpx;
background: rgba($bg-card, 0.9);
backdrop-filter: blur(20rpx);
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding: 0 10rpx;
}
.select-all {
display: flex;
align-items: center;
font-size: 28rpx;
color: #333;
}
.select-all .checkbox {
margin-right: 12rpx;
justify-content: center;
z-index: 100;
box-shadow: $shadow-sm;
padding: 0;
margin: 0;
border-radius: 0;
}
.tab-item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0;
border-radius: 12rpx;
transition: all 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
gap: 8rpx;
}
.tab-item.active {
background: #fff;
color: #007AFF;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.tab-text {
font-size: 28rpx;
}
.tab-count {
font-size: 24rpx;
opacity: 0.8;
}
.header { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
.status-text { text-align: center; color: #999; margin-top: 100rpx; }
.inventory-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.inventory-item {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
display: flex;
flex-direction: row;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}
.item-image {
width: 120rpx;
height: 120rpx;
margin-right: 24rpx;
margin-bottom: 0;
border-radius: 8rpx;
background-color: #f5f5f5;
flex-shrink: 0;
}
.item-info {
flex: 1;
text-align: left;
display: flex;
flex-direction: column;
justify-content: center;
}
.item-name {
font-size: 26rpx;
color: #333;
display: block;
margin-bottom: 4rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-count {
font-size: 24rpx;
color: #999;
margin-bottom: 4rpx;
}
.item-price {
font-size: 24rpx;
color: #ff4d4f;
}
.checkbox-area {
padding: 10rpx 20rpx 10rpx 0;
display: flex;
align-items: center;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ccc;
border-radius: 50%;
position: relative;
}
.checkbox.checked {
background-color: #007AFF;
border-color: #007AFF;
}
.checkbox.checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%) rotate(45deg);
width: 10rpx;
height: 20rpx;
border-right: 4rpx solid #fff;
border-bottom: 4rpx solid #fff;
}
.item-actions {
margin-top: 10rpx;
display: flex;
align-items: center;
}
.stepper {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 8rpx;
height: 48rpx;
}
.step-btn {
width: 48rpx;
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
font-size: 32rpx;
color: #666;
font-size: 28rpx;
color: $text-sub;
transition: all 0.3s;
padding: 0;
border-radius: 0;
&.active {
color: $brand-primary;
font-weight: 700;
font-size: 30rpx;
background: transparent;
box-shadow: none;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: $brand-primary;
border-radius: 4rpx;
}
}
}
.step-btn.minus { border-right: 1px solid #ddd; }
.step-btn.plus { border-left: 1px solid #ddd; }
.tab-count {
margin-left: 8rpx;
font-size: 20rpx;
background: rgba($brand-primary, 0.1);
padding: 2rpx 10rpx;
border-radius: 20rpx;
color: $brand-primary;
opacity: 1;
}
.step-num {
width: 60rpx;
/* 状态提示 */
.status-text {
padding-top: 200rpx;
text-align: center;
font-size: 26rpx;
color: #333;
color: $text-tertiary;
font-size: 28rpx;
margin-top: 0;
}
.loading-more, .no-more {
text-align: center;
padding: 30rpx;
color: $text-tertiary;
font-size: 24rpx;
}
/* Tab 0: 待处理列表 */
.action-bar {
margin-top: 108rpx; /* 88rpx tabs + 20rpx spacing */
padding: 0 $spacing-lg;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.select-all {
display: flex;
align-items: center;
font-size: 28rpx;
color: $text-main;
}
.inventory-grid {
padding: 0 $spacing-lg;
margin-top: 108rpx; /* default margin if no action bar */
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* Adjust margin if action bar exists */
.action-bar + .inventory-grid {
margin-top: 0;
}
.inventory-item {
background: $bg-card;
border-radius: $radius-lg;
padding: 24rpx;
margin-bottom: 0; /* handled by gap */
display: flex;
flex-direction: row;
align-items: center;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.02);
transition: all 0.2s ease;
animation: fadeInUp 0.4s ease-out backwards;
@for $i from 1 through 10 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.05}s;
}
}
&:active {
transform: scale(0.98);
}
}
.checkbox-area {
padding: 20rpx 20rpx 20rpx 0;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid $text-tertiary;
border-radius: 50%;
transition: all 0.2s;
&.checked {
background: $brand-primary;
border-color: $brand-primary;
position: relative;
box-shadow: 0 0 10rpx rgba($brand-primary, 0.3);
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18rpx;
height: 10rpx;
border-left: 3rpx solid #fff;
border-bottom: 3rpx solid #fff;
transform: translate(-50%, -65%) rotate(-45deg);
}
}
}
.item-image {
width: 140rpx;
height: 140rpx;
border-radius: $radius-md;
background: $bg-page;
flex-shrink: 0;
margin-right: 0; /* reset old margin */
}
.item-info {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 140rpx;
text-align: left; /* reset old text-align */
}
.item-name {
font-size: 28rpx;
font-weight: 600;
color: $text-main;
@include text-ellipsis(2);
margin-bottom: 0; /* reset old margin */
}
.item-price {
font-size: 24rpx;
color: $brand-primary;
margin-top: 8rpx;
font-weight: 600;
}
.item-actions {
margin-top: auto;
display: flex;
justify-content: flex-end;
align-items: center;
}
.item-count {
font-size: 28rpx;
color: $text-main;
font-weight: 600;
}
.stepper {
display: flex;
align-items: center;
background: $bg-page;
border-radius: $radius-sm;
padding: 4rpx;
height: auto; /* reset old height */
border: none; /* reset old border */
}
.step-btn {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $text-main;
background: transparent;
&.minus { color: $text-sub; border: none; }
&.plus { color: $brand-primary; border: none; }
&:active { opacity: 0.6; background: transparent !important; transform: scale(0.9); }
}
.step-num {
min-width: 60rpx;
text-align: center;
font-size: 28rpx;
font-weight: 600;
color: $text-main;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #fff;
background: rgba($bg-card, 0.95);
backdrop-filter: blur(20rpx);
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
z-index: 100;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
z-index: 99;
height: auto; /* reset old height */
animation: slideUp 0.3s ease-out;
}
.selected-info {
font-size: 28rpx;
color: #333;
font-weight: bold;
color: $text-main;
font-weight: 600;
}
.btn-group {
display: flex;
gap: 20rpx;
}
.action-btn {
margin: 0;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
border-radius: 32rpx;
height: 72rpx;
padding: 0 40rpx;
border-radius: $radius-round;
font-size: 28rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
margin: 0;
line-height: 1; /* reset line-height */
&:active { transform: scale(0.96); }
}
.btn-ship {
background-color: #f0ad4e;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
box-shadow: none; /* reset shadow */
}
.btn-redeem {
background: $gradient-brand;
color: #fff;
box-shadow: $shadow-warm;
animation: none; /* reset animation */
}
.btn-redeem {
background-color: #dd524d;
color: #fff;
/* Tab 1: 已申请发货 */
.shipment-list {
padding: 0 $spacing-lg;
margin-top: 108rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.shipment-card {
background: $bg-card;
border-radius: $radius-lg;
padding: 30rpx;
margin-bottom: 0; /* handled by gap */
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.02);
animation: fadeInUp 0.4s ease-out backwards;
}
.shipment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid rgba(0,0,0,0.05);
}
.shipment-batch {
display: flex;
align-items: center;
gap: 12rpx;
}
.batch-label {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
}
.batch-no {
font-size: 24rpx;
color: $text-sub;
font-family: monospace;
}
.count-badge {
font-size: 20rpx;
color: $brand-primary;
background: rgba($brand-primary, 0.1);
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
.shipment-status {
font-size: 24rpx;
font-weight: 600;
padding: 4rpx 16rpx;
border-radius: 20rpx;
&.status-pending { background: #FFF7E6; color: #FA8C16; }
&.status-shipped { background: #E6F7FF; color: #1890FF; }
&.status-delivered { background: #F6FFED; color: #52C41A; }
&.status-cancelled { background: #F5F5F5; color: #999; }
}
.product-thumbnails {
margin-bottom: 24rpx;
}
.thumb-scroll {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
overflow-x: auto;
}
.thumb-img {
width: 100rpx;
height: 100rpx;
border-radius: $radius-sm;
background: $bg-page;
flex-shrink: 0;
border: 1rpx solid rgba(0,0,0,0.05);
}
.thumb-more {
width: 100rpx;
height: 100rpx;
border-radius: $radius-sm;
background: $bg-page;
display: flex;
align-items: center;
justify-content: center;
color: $text-sub;
font-size: 24rpx;
font-weight: 600;
flex-shrink: 0;
}
.product-names {
font-size: 24rpx;
color: $text-sub;
line-height: 1.4;
display: block; /* reset flex */
}
.product-name-item {
margin-right: 12rpx;
background: transparent; /* reset bg */
padding: 0;
&:not(:last-child)::after {
content: '、';
}
}
.shipment-express {
background: $bg-page;
border-radius: $radius-md;
padding: 16rpx;
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.express-icon {
width: 60rpx;
height: 60rpx;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 16rpx;
}
.express-info {
flex: 1;
display: flex;
flex-direction: column;
}
.express-company {
font-size: 26rpx;
font-weight: 600;
color: $text-main;
}
.express-no {
font-size: 24rpx;
color: $text-sub;
margin-top: 4rpx;
}
.express-copy {
font-size: 22rpx;
color: $brand-primary;
padding: 6rpx 16rpx;
border: 1rpx solid $brand-primary;
border-radius: 20rpx;
background: transparent; /* reset bg */
box-shadow: none; /* reset shadow */
&:active { background: rgba($brand-primary, 0.05); }
}
.shipment-time {
display: flex;
flex-direction: column;
gap: 8rpx;
border-top: 1rpx dashed rgba(0,0,0,0.05);
padding-top: 20rpx;
}
.time-item {
font-size: 22rpx;
color: $text-tertiary;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.bottom-spacer {
height: 120rpx;
height: calc(120rpx + constant(safe-area-inset-bottom));
height: calc(120rpx + env(safe-area-inset-bottom));
}
</style>

View File

@ -1,44 +1,128 @@
<template>
<view class="page">
<view class="notice-bar">
<swiper class="notice-swiper" vertical circular autoplay interval="3000" duration="300">
<swiper-item v-for="n in displayNotices" :key="n.id">
<view class="notice-item">{{ n.text }}</view>
</swiper-item>
</swiper>
<!-- 背景装饰 -->
<view class="bg-decoration"></view>
<!-- 顶部导航栏 (搜索) -->
<view class="nav-header">
<view class="brand-logo">
<text class="brand-text">奇盒潮玩</text>
<view class="brand-star"></view>
</view>
<view class="search-bar">
<text class="search-icon">🔍</text>
<text class="search-placeholder">搜索商品</text>
</view>
</view>
<view class="banner-box">
<swiper class="banner-swiper" circular autoplay interval="4000" duration="500">
<swiper-item v-for="b in displayBanners" :key="b.id">
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
<view v-else class="banner-fallback">
<text class="banner-fallback-text">{{ b.title || '敬请期待' }}</text>
</view>
</swiper-item>
</swiper>
</view>
<view class="activity-section">
<view class="section-title">活动</view>
<view v-if="activityGroups.length">
<scroll-view class="tabs" scroll-x>
<view class="tab" v-for="g in activityGroups" :key="g.name" :class="{ active: g.name === selectedGroupName }" @tap="onSelectGroup(g.name)">{{ g.name }}</view>
</scroll-view>
<view v-if="activeGroupItems.length" class="activity-grid">
<view class="activity-item" v-for="a in activeGroupItems" :key="a.id" @tap="onActivityTap(a)">
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
<!-- 滚动区域 -->
<scroll-view class="main-content" scroll-y>
<!-- Banner 区域 -->
<view class="banner-box">
<swiper class="banner-swiper" circular autoplay interval="4000" duration="500">
<swiper-item v-for="b in displayBanners" :key="b.id">
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
<view v-else class="banner-fallback">
<text class="banner-fallback-text">{{ a.title || '活动敬请期待' }}</text>
<text class="banner-fallback-text">{{ b.title || '奇盒潮玩 V6.0' }}</text>
<text class="banner-tag">功能更新UI优化全面来袭</text>
</view>
</swiper-item>
</swiper>
</view>
<!-- 通知栏 -->
<view class="notice-bar" @tap="onNoticeTap">
<view class="notice-tag">通知</view>
<swiper class="notice-swiper" vertical circular autoplay interval="3000" duration="300">
<swiper-item v-for="n in displayNotices" :key="n.id">
<view class="notice-item">{{ n.text }}</view>
</swiper-item>
</swiper>
<view class="notice-more">查看详情</view>
</view>
<!-- 玩法分类专区 -->
<view class="gameplay-section">
<view class="section-header">
<text class="section-title">玩法分类</text>
</view>
<view class="gameplay-grid-v2">
<!-- 上排两大核心 -->
<view class="grid-row-top">
<view class="game-card-large card-yifan" @tap="navigateTo('/pages/activity/list/index?category=一番赏')">
<view class="card-bg-decoration"></view>
<view class="card-content-large">
<text class="card-title-large">一番赏</text>
<view class="card-tag-large">欧皇擂台</view>
<image class="card-mascot-large" src="https://via.placeholder.com/150/90EE90/000000?text=YI" mode="aspectFit" />
</view>
</view>
<view class="game-card-large card-wuxian" @tap="navigateTo('/pages/activity/list/index?category=无限赏')">
<view class="card-content-large">
<text class="card-title-large">无限赏</text>
<view class="card-tag-large yellow">一发入魂</view>
<image class="card-mascot-large" src="https://via.placeholder.com/150/FFD700/000000?text=WU" mode="aspectFit" />
</view>
</view>
</view>
<!-- 下排三小功能 -->
<view class="grid-row-bottom">
<view class="game-card-small card-match" @tap="navigateTo('/pages/activity/list/index?category=对对碰')">
<text class="card-title-small">对对碰</text>
<text class="card-subtitle-small">碰一碰消除</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/FFB6C1/000000?text=Match" mode="aspectFit" />
</view>
<view class="game-card-small card-tower" @tap="navigateTo('/pages/activity/list/index?category=爬塔')">
<text class="card-title-small">爬塔</text>
<text class="card-subtitle-small">层层挑战</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Tower" mode="aspectFit" />
</view>
<view class="game-card-small card-more" @tap="navigateTo('#')">
<text class="card-title-small">更多</text>
<text class="card-subtitle-small">敬请期待</text>
<image class="card-icon-small" src="https://via.placeholder.com/80/E0E0E0/000000?text=More" mode="aspectFit" />
</view>
<text class="activity-name">{{ a.title }}</text>
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
</view>
</view>
<view v-else class="activity-empty">该分组暂无活动</view>
</view>
<view v-else class="activity-empty">暂无活动</view>
</view>
<!-- 推荐活动列表 -->
<view class="activity-section">
<view class="section-header">
<text class="section-title">推荐活动</text>
</view>
<view v-if="activeGroupItems.length" class="activity-grid-list">
<view class="activity-item" v-for="a in activeGroupItems" :key="a.id" @tap="onActivityTap(a)">
<view class="activity-thumb-box">
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
<view v-else class="banner-fallback mini">
<text class="banner-fallback-text mini">{{ a.title }}</text>
</view>
<!-- 热门标签 -->
<view class="activity-tag-hot">HOT</view>
</view>
<view class="activity-info">
<text class="activity-name">{{ a.title }}</text>
<view class="activity-row">
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
<view class="activity-btn-go">GO</view>
</view>
</view>
</view>
</view>
<view v-else class="activity-empty">暂无更多活动</view>
</view>
<!-- 底部垫高 -->
<view style="height: 40rpx"></view>
</scroll-view>
</view>
</template>
@ -80,10 +164,8 @@ export default {
return Array.from(map.entries()).map(([name, items]) => ({ name, items }))
},
activeGroupItems() {
const groups = this.activityGroups
const name = this.selectedGroupName || (groups[0] && groups[0].name) || ''
const g = groups.find(x => x.name === name)
return g ? g.items : []
// Return ALL activities without filtering by group
return Array.isArray(this.activities) ? this.activities : []
}
},
onShow() {
@ -109,11 +191,7 @@ export default {
this.selectedGroupName = String(name || '')
},
updateSelectedGroup() {
const groups = this.activityGroups
if (!groups.length) { this.selectedGroupName = ''; return }
if (!groups.find(g => g.name === this.selectedGroupName)) {
this.selectedGroupName = groups[0].name
}
// No-op as we now show all
},
toArray(x) { return Array.isArray(x) ? x : [] },
unwrap(list) {
@ -142,7 +220,6 @@ export default {
},
normalizeBanners(list) {
const arr = this.unwrap(list)
console.log('normalizeBanners input', list, 'unwrapped', arr)
const mapped = arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? '',
@ -151,12 +228,10 @@ export default {
sort: typeof i.sort === 'number' ? i.sort : 0
})).filter(i => i.image)
mapped.sort((a, b) => a.sort - b.sort)
console.log('normalizeBanners mapped', mapped)
return mapped
},
normalizeActivities(list) {
const arr = this.unwrap(list)
console.log('normalizeActivities input', list, 'unwrapped', arr)
const mapped = arr.map((i, idx) => ({
id: i.id ?? String(idx),
image: this.cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
@ -166,7 +241,6 @@ export default {
category_name: (i.category_name ?? i.categoryName ?? '').trim(),
category_id: i.activity_category_id ?? i.category_id ?? i.categoryId ?? null
})).filter(i => i.image || i.title)
console.log('normalizeActivities mapped', mapped)
return mapped
},
buildActivitySubtitle(i) {
@ -178,36 +252,29 @@ export default {
return parts.join(' · ')
},
async loadHomeData() {
const results = await Promise.allSettled([
this.apiGet('/api/app/notices'),
this.apiGet('/api/app/banners'),
this.apiGet('/api/app/activities')
])
const [nRes, bRes, acRes] = results
if (nRes.status === 'fulfilled') {
console.log('notices ok', nRes.value)
this.notices = this.normalizeNotices(nRes.value)
} else {
console.error('notices error', nRes.reason)
// Notices
try {
const nData = await this.apiGet('/api/app/notices')
this.notices = this.normalizeNotices(nData)
} catch (e) {
this.notices = []
}
if (bRes.status === 'fulfilled') {
console.log('banners ok', bRes.value)
this.banners = this.normalizeBanners(bRes.value)
} else {
console.error('banners error', bRes.reason)
// Banners
try {
const bData = await this.apiGet('/api/app/banners')
this.banners = this.normalizeBanners(bData)
} catch (e) {
this.banners = []
}
if (acRes.status === 'fulfilled') {
console.log('activities ok', acRes.value)
this.activities = this.normalizeActivities(acRes.value)
this.updateSelectedGroup()
} else {
console.error('activities error', acRes.reason)
// Activities
try {
const acData = await this.apiGet('/api/app/activities')
this.activities = this.normalizeActivities(acData)
} catch (e) {
this.activities = []
this.updateSelectedGroup()
}
console.log('home normalized', { notices: this.notices, banners: this.banners, activities: this.activities })
},
onBannerTap(b) {
const imgs = (Array.isArray(this.banners) ? this.banners : []).map(x => x.image).filter(Boolean)
@ -224,9 +291,11 @@ export default {
const name = (a.category_name || a.categoryName || '').trim()
const id = a.id
let path = ''
if (name === '一番赏') path = '/pages/activity/yifanshang/index'
else if (name === '无限赏') path = '/pages/activity/wuxianshang/index'
else if (name === '对对碰') path = '/pages/activity/duiduipeng/index'
if (name.includes('一番赏')) path = '/pages/activity/yifanshang/index'
else if (name.includes('无限赏')) path = '/pages/activity/wuxianshang/index'
else if (name.includes('对对碰')) path = '/pages/activity/duiduipeng/index'
else if (name.includes('爬塔')) path = '/pages/activity/pata/index'
if (path && id) {
uni.navigateTo({ url: `${path}?id=${id}` })
return
@ -234,29 +303,652 @@ export default {
if (a.link && /^\/.+/.test(a.link)) {
uni.navigateTo({ url: a.link })
}
},
navigateTo(url) {
if(url === '#') return
uni.navigateTo({ url })
},
onNoticeTap() {
const content = this.displayNotices.map(n => n.text).join('\n')
uni.showModal({
title: '系统通知',
content: content || '暂无通知',
showCancel: false,
confirmText: '知道了'
})
}
},
//
onShareAppMessage() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '奇盒潮玩 - 开箱惊喜等你来',
path: `/pages/index/index?invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
},
//
onShareTimeline() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '奇盒潮玩 - 开箱惊喜等你来',
query: `invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
}
}
</script>
<style>
.page { padding: 24rpx }
.notice-bar { height: 64rpx; background: #fff4e6; border-radius: 8rpx; overflow: hidden; margin-bottom: 24rpx; }
.notice-swiper { height: 64rpx }
.notice-item { height: 64rpx; line-height: 64rpx; padding: 0 24rpx; color: #a15c00; font-size: 26rpx }
.banner-box { margin-bottom: 24rpx }
.banner-swiper { width: 100%; height: 320rpx; border-radius: 12rpx; overflow: hidden }
.banner-image { width: 100%; height: 320rpx }
.banner-fallback { width: 100%; height: 320rpx; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f5f5f5, #eaeaea) }
.banner-fallback-text { color: #666; font-size: 28rpx }
.activity-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx }
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
.tabs { white-space: nowrap; display: flex; gap: 12rpx; margin-bottom: 16rpx }
.tab { display: inline-flex; align-items: center; height: 56rpx; padding: 0 20rpx; border-radius: 999rpx; background: #f5f7fa; color: #555; font-size: 26rpx }
.tab.active { background: #007AFF; color: #fff }
.activity-grid { display: flex; flex-wrap: wrap; margin: -12rpx }
.activity-item { width: 50%; padding: 12rpx }
.activity-thumb { width: 100%; height: 200rpx; border-radius: 8rpx }
.activity-name { display: block; margin-top: 8rpx; font-size: 26rpx; color: #222 }
.activity-desc { display: block; margin-top: 4rpx; font-size: 22rpx; color: #888 }
<style lang="scss">
/* ============================================
奇盒潮玩 - 首页样式 (V6.0 Pro Refined)
============================================ */
.page {
padding: 0;
background-color: $bg-page;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ========== 顶部导航栏 ========== */
.nav-header {
display: flex;
align-items: center;
padding: $spacing-md $spacing-lg;
background: transparent; /* 透明背景,依靠内容区的渐变 */
padding-top: calc($spacing-md + env(safe-area-inset-top));
gap: $spacing-lg;
z-index: 10;
}
.brand-logo {
display: flex;
align-items: center;
}
.brand-text {
font-size: 40rpx;
font-weight: 900;
color: $text-main;
font-style: italic;
letter-spacing: -1rpx;
text-shadow: 0 2rpx 4rpx rgba(255, 255, 255, 0.5);
}
.brand-star {
font-size: 24rpx;
margin-left: 4rpx;
margin-top: -16rpx;
animation: pulse 2s infinite;
}
.search-bar {
flex: 1;
height: 72rpx;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: $radius-round;
display: flex;
align-items: center;
padding: 0 $spacing-lg;
border: 1rpx solid rgba(255, 255, 255, 0.6);
box-shadow: $shadow-sm;
transition: all 0.3s ease;
}
.search-bar:active {
background: #fff;
transform: scale(0.98);
}
.search-icon { margin-right: 12rpx; font-size: 32rpx; }
.search-placeholder { color: $text-tertiary; font-size: 28rpx; font-weight: 500; }
/* ========== 滚动主内容区 ========== */
.main-content {
flex: 1;
/* background: linear-gradient(180deg, $bg-secondary 0%, $bg-page 400rpx); */ /* 移除原有背景,使用全局背景装饰 */
position: relative;
z-index: 1; /* 确保内容在背景装饰之上 */
padding-top: $spacing-sm;
}
/* Logo Banner */
.banner-box {
margin: 0 $spacing-lg $spacing-lg;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-float;
transform: translateZ(0); /* 开启硬件加速 */
animation: fadeInUp 0.6s ease-out;
}
.banner-swiper, .banner-image, .banner-fallback {
width: 100%;
height: 340rpx; /* 略微增高 */
}
.banner-fallback {
background: linear-gradient(135deg, $bg-secondary, #FFF0D6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 4rpx dashed $brand-primary-light;
}
.banner-fallback-text {
font-size: 48rpx;
font-weight: 900;
color: $brand-primary;
font-style: italic;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 0 rgba(255,255,255,1);
}
.banner-tag {
background: #1A1A1A;
color: $accent-gold;
padding: 6rpx 20rpx;
border-radius: $radius-round;
font-size: 24rpx;
font-weight: 700;
}
/* 通知栏 */
.notice-bar {
margin: 0 $spacing-lg $spacing-xl;
background: #FFFFFF;
border-radius: $radius-round;
padding: 16rpx 24rpx;
display: flex;
align-items: center;
gap: 16rpx;
box-shadow: $shadow-sm;
animation: fadeInUp 0.6s ease-out 0.1s backwards;
}
.notice-tag {
background: $gradient-brand;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 700;
box-shadow: 0 2rpx 6rpx rgba($brand-primary, 0.3);
}
.notice-swiper { flex: 1; height: 36rpx; }
.notice-item {
font-size: 26rpx;
color: $text-main;
line-height: 36rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
}
.notice-more { font-size: 24rpx; color: $text-sub; display: flex; align-items: center; }
.notice-more::after { content: ''; margin-left: 4rpx; font-size: 32rpx; line-height: 24rpx; }
/* 玩法专区 - 方案B2+3 杂志风布局 */
.gameplay-section {
padding: 0 $spacing-lg;
margin-bottom: $spacing-xl;
animation: fadeInUp 0.6s ease-out 0.2s backwards;
}
.section-header {
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title {
font-size: 36rpx;
font-weight: 900;
color: $text-main;
position: relative;
z-index: 1;
padding-left: 12rpx;
font-style: italic;
}
/* 标题装饰竖线 */
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%) skewX(-10deg);
width: 8rpx;
height: 32rpx;
background: $brand-primary;
border-radius: 2rpx;
}
.gameplay-grid-v2 {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 上排 */
.grid-row-top {
display: flex;
gap: 20rpx;
height: 220rpx; /* 增加高度,展示更多细节 */
}
.game-card-large {
flex: 1;
border-radius: $radius-lg;
position: relative;
overflow: hidden;
padding: 28rpx;
box-shadow: $shadow-card;
transition: transform 0.2s;
}
.game-card-large:active {
transform: scale(0.98);
}
/* 下排 */
.grid-row-bottom {
display: flex;
gap: 20rpx;
height: 160rpx;
}
.game-card-small {
flex: 1;
border-radius: $radius-md;
position: relative;
overflow: hidden;
padding: 20rpx;
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: $shadow-sm;
background: white;
transition: all 0.2s;
}
.game-card-small:active {
transform: scale(0.96);
box-shadow: none;
}
/* 内容样式 - 大卡片 */
.card-content-large {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.card-title-large {
font-size: 40rpx;
font-weight: 900;
color: #FFF;
font-style: italic;
margin-bottom: 16rpx;
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
}
.card-tag-large {
font-size: 22rpx;
background: rgba(255, 255, 255, 0.9);
color: $text-main;
padding: 6rpx 16rpx;
border-radius: $radius-round;
font-weight: 800;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
backdrop-filter: blur(4px);
}
.card-tag-large.yellow { color: #D97706; }
.card-mascot-large {
position: absolute;
right: -20rpx;
bottom: -30rpx;
width: 180rpx;
height: 180rpx;
transform: rotate(10deg);
filter: drop-shadow(0 8rpx 16rpx rgba(0,0,0,0.2));
}
/* 内容样式 - 小卡片 */
.card-title-small {
font-size: 30rpx;
font-weight: 800;
color: $text-main;
margin-bottom: 6rpx;
z-index: 2;
}
.card-subtitle-small {
font-size: 22rpx;
color: $text-sub;
z-index: 2;
}
.card-icon-small {
position: absolute;
right: -10rpx;
bottom: -10rpx;
width: 100rpx;
height: 100rpx;
opacity: 0.9;
transform: rotate(-10deg);
}
/* 背景配色 - 优化后的渐变 */
.card-yifan {
background: linear-gradient(135deg, $brand-primary 0%, $brand-secondary 100%); /* 品牌橙渐变 */
}
.card-wuxian {
background: $gradient-gold; /* 质感金渐变 */
}
.card-match {
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%); /* 柔和粉 */
}
.card-match .card-title-small { color: $accent-pink; }
.card-tower {
background: linear-gradient(135deg, #E0C3FC 0%, #8EC5FC 100%); /* 梦幻紫蓝 */
}
.card-tower .card-title-small { color: $accent-purple; }
.card-more {
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
}
.card-more .card-title-small { color: $text-sub; }
/* 推荐活动列表 */
.activity-section {
padding: 0 $spacing-lg;
animation: fadeInUp 0.6s ease-out 0.3s backwards;
}
.activity-grid-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.activity-item {
background: #FFFFFF;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-card;
display: flex;
flex-direction: column;
transition: transform 0.2s;
}
.activity-item:active {
transform: scale(0.98);
}
.activity-thumb-box {
position: relative;
width: 100%;
padding-bottom: 100%; /* 1:1 正方形 */
}
.activity-thumb {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background: #EEE;
}
.activity-tag-hot {
position: absolute;
top: 16rpx;
left: 16rpx;
background: $gradient-brand;
color: #fff;
font-size: 20rpx;
padding: 6rpx 14rpx;
border-radius: 8rpx;
font-weight: 800;
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
animation: pulse 2s infinite;
}
.activity-info {
padding: 24rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.activity-name {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 20rpx;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.activity-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.activity-desc {
font-size: 26rpx;
color: $accent-red; /* 价格/热度颜色 */
font-weight: 800;
}
.activity-btn-go {
background: $text-main;
color: #FFD700;
font-size: 20rpx;
font-weight: 900;
padding: 8rpx 24rpx;
border-radius: $radius-round;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
}
/* 空状态 */
.activity-empty {
text-align: center;
padding: 80rpx 0;
color: $text-tertiary;
font-size: 28rpx;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.9; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8rpx); }
}
/* Banner 入场动画 */
.banner-box {
animation: scaleIn 0.5s ease-out;
}
/* 通知栏滑入 */
.notice-bar {
animation: fadeInUp 0.4s ease-out 0.1s both;
}
/* 玩法卡片交错入场 */
.game-card-large:first-child {
animation: fadeInUp 0.4s ease-out 0.2s both;
}
.game-card-large:last-child {
animation: fadeInUp 0.4s ease-out 0.3s both;
}
.game-card-small:nth-child(1) {
animation: fadeInUp 0.4s ease-out 0.35s both;
}
.game-card-small:nth-child(2) {
animation: fadeInUp 0.4s ease-out 0.4s both;
}
.game-card-small:nth-child(3) {
animation: fadeInUp 0.4s ease-out 0.45s both;
}
/* 活动卡片交错入场 */
.activity-item:nth-child(1) { animation: fadeInUp 0.4s ease-out 0.2s both; }
.activity-item:nth-child(2) { animation: fadeInUp 0.4s ease-out 0.25s both; }
.activity-item:nth-child(3) { animation: fadeInUp 0.4s ease-out 0.3s both; }
.activity-item:nth-child(4) { animation: fadeInUp 0.4s ease-out 0.35s both; }
.activity-item:nth-child(n+5) { animation: fadeInUp 0.4s ease-out 0.4s both; }
/* 玻璃态搜索栏 */
.search-bar {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(255, 255, 255, 0.6);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.search-bar:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
/* 卡片点击效果 */
.game-card-large,
.game-card-small {
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
cursor: pointer;
}
.game-card-large:active {
transform: scale(0.97);
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
}
.game-card-small:active {
transform: scale(0.95);
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08);
}
/* 活动卡片悬浮效果 */
.activity-item {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.activity-item:active {
transform: scale(0.97) translateY(4rpx);
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
}
/* GO 按钮脉冲动画 */
.activity-btn-go {
animation: pulse 2s infinite;
transition: all 0.2s ease;
}
.activity-btn-go:active {
animation: none;
transform: scale(0.9);
}
/* 图片加载动画 */
.activity-thumb {
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* 热门标签浮动效果 */
.activity-tag-hot {
animation: float 3s ease-in-out infinite;
}
/* Banner 图片悬浮效果 */
.banner-image {
transition: transform 0.4s ease;
}
.banner-box:active .banner-image {
transform: scale(1.02);
}
/* 通知栏点击效果 */
.notice-bar {
transition: all 0.2s ease;
}
.notice-bar:active {
background: #F0F0F0;
transform: scale(0.99);
}
/* 品牌标志动画 */
.brand-star {
animation: float 2s ease-in-out infinite;
}
/* Section 标题增强 */
.section-header {
animation: fadeInUp 0.4s ease-out 0.15s both;
}
/* 增强阴影效果 */
.banner-box {
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.08);
}
.activity-item {
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);
}
/* 热门标签增强 */
.activity-tag-hot {
background: linear-gradient(135deg, rgba(255,77,79,0.9), rgba(255,107,53,0.9));
color: #fff;
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.2);
}
</style>

View File

@ -1,74 +1,105 @@
<template>
<view class="container">
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
<!-- #ifdef MP-TOUTIAO -->
<view class="login-form">
<view class="input-row">
<text class="label">账号</text>
<input
type="text"
v-model="account"
class="input-field"
placeholder="请输入账号"
/>
</view>
<view class="input-row">
<text class="label">密码</text>
<input
type="password"
v-model="pwd"
class="input-field"
placeholder="请输入密码"
/>
</view>
<!-- 记住账号密码 -->
<view class="remember-row">
<checkbox :value="remember" @change="handleRememberChange" />
<text class="remember-text">记住账号密码</text>
</view>
<!-- 按钮区域 -->
<view class="button-group">
<button class="btn login-btn" @click="handleLogin">登录</button>
</view>
<!-- 注册链接 -->
<view class="register-link">
<text class="register-text" @click="goToRegister">还没有账号点击注册</text>
</view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="title">微信登录</view>
<button class="btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">授权手机号快速登录</button>
<!-- #endif -->
<view class="agreements">
<text>注册或登录即表示您已阅读并同意</text>
<text class="link" @tap="toUserAgreement">用户协议</text>
<text></text>
<text class="link" @tap="toPurchaseAgreement">购买协议</text>
<!-- 装饰球体 -->
<view class="orb orb-1"></view>
<view class="orb orb-2"></view>
<view class="content-wrap">
<view class="glass-card">
<!-- 品牌Logo -->
<view class="brand-section">
<view class="logo-box">
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
</view>
<view class="app-name">奇盒潮玩</view>
<view class="welcome-text">开启欧气之旅 </view>
</view>
<!-- 登录表单 -->
<!-- #ifdef MP-TOUTIAO -->
<view class="login-form">
<view class="input-group">
<view class="input-icon">
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjAgMjF2LTJhNCA0IDAgMCAwLTQtNEg4YTQgNCAwIDAgMC00IDR2MiIgLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<input
type="text"
v-model="account"
class="input-field"
placeholder="请输入账号"
placeholder-class="input-placeholder"
/>
</view>
<view class="input-group">
<view class="input-icon">
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB4PSIzIiB5PS“xMSIgd2lkdGg9“MTgiIGhlaWdodD0iMTEiIHJ4PSIyIiByeT0iMiIgLz48cGF0aCBkPSJNNyAxMVY3YTUgNSAwIDAgMSAxMCAwdjQiIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<input
type="password"
v-model="pwd"
class="input-field"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
</view>
<view class="options-row">
<view class="remember-box" @click="toggleRemember">
<view class="checkbox" :class="{ checked: remember }">
<view class="check-mark" v-if="remember"></view>
</view>
<text class="remember-text">记住密码</text>
</view>
</view>
<button class="btn login-btn" @click="handleLogin">
<text class="btn-text">立即登录</text>
<view class="btn-shine"></view>
</button>
<view class="register-link">
<text class="register-text" @click="goToRegister">没有账号<text class="highlight">立即注册</text></text>
</view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="weixin-login-box">
<button class="btn weixin-btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">
<image class="wx-icon" src="/static/logo.png" mode="aspectFit"></image> <!-- 应该用微信图标暂时用logo代替或SVG -->
<text>微信一键登录</text>
</button>
</view>
<!-- #endif -->
<!-- 协议区 -->
<view class="agreements">
<view class="checkbox-area">
<view class="checkbox round" :class="{ checked: agreementChecked }" @click="toggleAgreement"></view>
</view>
<view class="agreement-text">
登录即代表同意 <text class="link" @tap="toUserAgreement">用户协议</text> & <text class="link" @tap="toPurchaseAgreement">隐私政策</text>
</view>
</view>
</view>
<view v-if="error" class="error-toast">{{ error }}</view>
</view>
<view v-if="needBindPhone" class="tip">登录成功请绑定手机号以完成登录</view>
<view v-if="error" class="error">{{ error }}</view>
</view>
</template>
<script setup>
import { ref, computed,onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
const loading = ref(false)
const error = ref('')
const needBindPhone = ref(false)
const account =ref("")
const pwd = ref ("")
const remember=ref(false)
const loggedIn = computed(() => !!uni.getStorageSync('token'))
const account = ref("")
const pwd = ref("")
const remember = ref(false)
const agreementChecked = ref(false) // false
onMounted(() => {
try {
@ -83,120 +114,97 @@ onMounted(() => {
}
})
//
const handleRememberChange = (e) => {
remember.value = e.detail.value.length > 0
function toggleRemember() {
remember.value = !remember.value
}
function toggleAgreement() {
agreementChecked.value = !agreementChecked.value
}
function goToRegister() { uni.navigateTo({ url: '/pages/register/register' }) }
function onLogin() {}
function handleLogin() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
return
}
// TODO: Implement actual username/password login logic if API available
uni.showToast({ title: '普通登录逻辑待接入', icon: 'none' })
}
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/agreement/purchase' }) }
function onGetPhoneNumber(e) {
if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
return
}
const phoneCode = e.detail.code
console.log('login_flow start getPhoneNumber, codeExists:', !!phoneCode)
if (!phoneCode) {
uni.showToast({ title: '未授权手机号', icon: 'none' })
console.error('login_flow error: missing phoneCode')
return
}
loading.value = true
error.value = ''
uni.login({
provider: 'weixin',
success: async (res) => {
try {
const loginCode = res.code
console.log('login_flow uni.login success, loginCode exists:', !!loginCode)
const inviterCode = uni.getStorageSync('inviter_code')
console.log('login_flow using inviter_code:', inviterCode)
const data = await wechatLogin(loginCode, inviterCode)
console.log('login_flow wechatLogin response user_id:', data && data.user_id)
const token = data && data.token
const user_id = data && data.user_id
const avatar = data && data.avatar
const nickname = data && data.nickname
const invite_code = data && data.invite_code
const openid = data && data.openid
uni.setStorageSync('user_info', data || {})
if (token) {
uni.setStorageSync('token', token)
console.log('login_flow token stored')
}
if (user_id) {
uni.setStorageSync('user_id', user_id)
console.log('login_flow user_id stored:', user_id)
}
if (avatar) {
uni.setStorageSync('avatar', avatar)
}
if (nickname) {
uni.setStorageSync('nickname', nickname)
}
if (invite_code) {
uni.setStorageSync('invite_code', invite_code)
}
if (openid) {
uni.setStorageSync('openid', openid)
}
console.log('login_flow bindPhone start')
try {
// token
const user_info = data || {}
uni.setStorageSync('user_info', user_info)
if (token) uni.setStorageSync('token', token)
if (user_id) uni.setStorageSync('user_id', user_id)
if (user_info.avatar) uni.setStorageSync('avatar', user_info.avatar)
if (user_info.nickname) uni.setStorageSync('nickname', user_info.nickname)
if (user_info.invite_code) uni.setStorageSync('invite_code', user_info.invite_code)
// openid
const openid = data && (data.openid || data.open_id)
if (openid) uni.setStorageSync('openid', openid)
// ...
try {
await new Promise(r => setTimeout(r, 600))
const bindRes = await bindPhone(user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
const phoneNumber = (bindRes && (bindRes.phone || bindRes.phone_number || bindRes.mobile)) || ''
if (phoneNumber) uni.setStorageSync('phone_number', phoneNumber)
} catch (bindErr) {
if (bindErr && bindErr.statusCode === 401) {
console.warn('login_flow bindPhone 401, try re-login and retry')
// code
const relogin = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
const data2 = await wechatLogin(relogin.code, inviterCode)
const token2 = data2 && data2.token
const user2 = data2 && data2.user_id
if (token2) uni.setStorageSync('token', token2)
if (user2) uni.setStorageSync('user_id', user2)
//
await new Promise(r => setTimeout(r, 600))
const bindRes2 = await bindPhone(user2 || user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
const phoneNumber2 = (bindRes2 && (bindRes2.phone || bindRes2.phone_number || bindRes2.mobile)) || ''
if (phoneNumber2) uni.setStorageSync('phone_number', phoneNumber2)
} else {
throw bindErr
}
//
console.warn('Bind phone failed', bindErr)
}
uni.setStorageSync('phone_bound', true)
console.log('login_flow bindPhone success, phone_bound stored')
//
try {
const stats = await getUserStats(user_id)
console.log('login_flow getUserStats success')
const balance = await getPointsBalance(user_id)
console.log('login_flow getPointsBalance success')
uni.setStorageSync('user_stats', stats)
const balance = await getPointsBalance(user_id)
const b = balance && balance.balance !== undefined ? balance.balance : balance
uni.setStorageSync('points_balance', b)
} catch (e) {
console.error('login_flow fetch stats/points error:', e && (e.message || e.errMsg))
}
uni.showToast({ title: '登录并绑定成功', icon: 'success' })
console.log('login_flow navigate to index')
uni.reLaunch({ url: '/pages/index/index' })
uni.setStorageSync('points_balance', b)
} catch(e) {}
uni.showToast({ title: '欢迎回来', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/mine/index' }) // Redirect to Mine page
}, 500)
} catch (err) {
console.error('login_flow error:', err && (err.message || err.errMsg), 'status:', err && err.statusCode)
error.value = err.message || '登录或绑定失败'
error.value = err.message || '登录失败'
} finally {
loading.value = false
}
},
fail: (e) => {
console.error('login_flow uni.login fail:', e && e.errMsg)
fail: () => {
error.value = '微信登录失败'
loading.value = false
}
@ -204,63 +212,244 @@ function onGetPhoneNumber(e) {
}
</script>
<style scoped>
.login-form {
padding: 20px;
}
.input-row {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.remember-row {
display: flex;
align-items: center;
margin-bottom: 60rpx;
}
.remember-text {
margin-left: 16rpx;
font-size: 28rpx;
color: #666;
}
.register-link {
text-align: center;
}
.register-text {
font-size: 26rpx;
color: #999;
text-decoration: underline; /* 可选:加下划线 */
}
.register-text:active {
color: #ff6700; /* 点击时变色 */
}
.label {
width: 60px; /* 固定宽度,保证对齐 */
font-size: 16px;
color: #333;
}
.input-field {
flex: 1;
height: 40px;
border: 2px solid #ccc; /* 边框加粗 */
border-radius: 4px;
padding: 0 10px;
font-size: 16px;
/* 去除 iOS 默认样式 */
-webkit-appearance: none;
outline: none;
}
.container { padding: 40rpx; display: flex; flex-direction: column; align-items: center }
.logo { width: 200rpx; margin-top: 100rpx; margin-bottom: 40rpx }
.title { font-size: 36rpx; margin-bottom: 20rpx }
.btn { width: 80%; margin-top: 20rpx }
.agreements { margin-top: 16rpx; font-size: 24rpx; color: #666; display: flex; flex-wrap: wrap; justify-content: center }
.link { color: #007AFF; margin: 0 6rpx }
.tip { color: #666; margin-top: 12rpx }
.error { color: #e43; margin-top: 20rpx }
</style>
<style lang="scss" scoped>
/* Page Container */
.container {
min-height: 100vh;
position: relative;
background: $bg-secondary; /* Use secondary for better depth with orbs */
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Orbs Background */
.bg-decoration {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80rpx); /* Increased blur for smoother look */
opacity: 0.6;
}
.orb-1 {
width: 500rpx;
height: 500rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.4), transparent 70%);
top: -100rpx;
left: -100rpx;
animation: float 8s ease-in-out infinite;
}
.orb-2 {
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba($accent-gold, 0.3), transparent 70%);
bottom: -150rpx;
right: -150rpx;
animation: float 10s ease-in-out infinite reverse;
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 40rpx); }
}
.content-wrap {
position: relative;
z-index: 1;
padding: 0 40rpx;
width: 100%;
box-sizing: border-box;
animation: fadeInUp 0.6s ease-out;
}
.glass-card {
background: $bg-glass;
backdrop-filter: blur(30rpx);
border-radius: 40rpx;
padding: 60rpx 40rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.8);
}
/* Brand Section */
.brand-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60rpx;
}
.logo-box {
width: 160rpx;
height: 160rpx;
background: $bg-card;
border-radius: 40rpx;
padding: 20rpx;
box-shadow: 0 12rpx 30rpx rgba($brand-primary, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-xl;
animation: pulse 3s infinite;
}
.logo { width: 100%; height: 100%; }
.app-name {
font-size: 44rpx;
font-weight: 900;
color: $text-main;
margin-bottom: 12rpx;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
}
.welcome-text {
font-size: 26rpx;
color: $text-sub;
letter-spacing: 4rpx;
text-transform: uppercase;
opacity: 0.8;
}
/* Form Styles */
.input-group {
background: $bg-card;
border-radius: $radius-round;
height: 100rpx;
display: flex;
align-items: center;
padding: 0 32rpx;
margin-bottom: $spacing-xl;
border: 2rpx solid transparent;
transition: all 0.3s;
box-shadow: $shadow-sm;
&.glass-input {
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.5);
&:focus-within {
background: $bg-card;
border-color: $brand-primary;
box-shadow: 0 0 0 4rpx rgba($brand-primary, 0.15);
transform: translateY(-2rpx);
}
}
}
.input-icon { width: 40rpx; height: 40rpx; margin-right: 20rpx; opacity: 1; }
.input-icon image { width: 100%; height: 100%; }
.input-field { flex: 1; height: 100%; font-size: 30rpx; color: $text-main; font-weight: 500; }
.input-placeholder { color: $text-tertiary; font-weight: 400; }
.options-row { display: flex; justify-content: space-between; margin-bottom: 60rpx; padding: 0 10rpx; }
.remember-box { display: flex; align-items: center; }
.checkbox {
width: 36rpx; height: 36rpx;
border: 3rpx solid $border-color;
border-radius: 10rpx;
margin-right: 12rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
background: rgba(255,255,255,0.5);
&.checked {
background: $brand-primary;
border-color: $brand-primary;
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
}
}
.check-mark { color: $text-inverse; font-size: $font-sm; font-weight: bold; }
.remember-text { font-size: 26rpx; color: $text-sub; }
/* Buttons */
.btn {
height: 96rpx;
border-radius: $radius-round;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-lg;
font-weight: 800;
position: relative;
overflow: hidden;
transition: all 0.2s;
&:active { transform: scale(0.96); }
}
.login-btn {
background: $gradient-brand;
color: $text-inverse;
box-shadow: 0 10rpx 30rpx rgba($brand-primary, 0.3);
margin-bottom: $spacing-xl;
border: none;
}
.btn-shine {
position: absolute;
top: 0; left: -100%;
width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transform: skewX(-20deg);
animation: shine 3s infinite;
}
@keyframes shine {
0% { left: -100%; }
20% { left: 200%; }
100% { left: 200%; }
}
.weixin-btn {
background: #07C160; /* WeChat Brand Color */
color: $text-inverse;
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
border: none;
}
.wx-icon { width: 48rpx; height: 48rpx; margin-right: 16rpx; filter: brightness(100); } /* Make logo white if it's the logo, but ideally it should be WeChat icon */
/* Register Link */
.register-link { text-align: center; margin-top: $spacing-xl; }
.register-text { font-size: $font-md; color: $text-sub; }
.highlight { color: $brand-primary; font-weight: 700; margin-left: 8rpx; }
/* Agreements */
.agreements {
margin-top: 60rpx;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0 20rpx;
}
.checkbox.round { border-radius: 50%; width: 32rpx; height: 32rpx; margin-top: 4rpx; }
.checkbox-area { padding-right: 12rpx; }
.agreement-text {
font-size: $font-sm;
color: $text-tertiary;
line-height: 1.5;
text-align: left;
}
.link { color: $brand-primary; text-decoration: none; font-weight: 600; margin: 0 4rpx; }
.error-toast {
position: fixed;
top: 100rpx;
left: 50%;
transform: translateX(-50%);
background: rgba($uni-color-error, 0.9);
color: $text-inverse;
padding: 16rpx 32rpx;
border-radius: 12rpx;
font-size: 26rpx;
z-index: 999;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.2);
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from { transform: translate(-50%, -100%); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,15 @@
<template>
<view class="wrap">
<view class="bg-decoration"></view>
<view class="tabs">
<view class="tab" :class="{ active: currentTab === 'pending' }" @click="switchTab('pending')">待付款</view>
<view class="tab" :class="{ active: currentTab === 'completed' }" @click="switchTab('completed')">已完成</view>
</view>
<view v-if="error" class="error">{{ error }}</view>
<view v-if="orders.length === 0 && !loading" class="empty">暂无订单</view>
<view v-if="orders.length === 0 && !loading" class="empty">
<view class="empty-icon">📦</view>
<view class="empty-text">暂无订单</view>
</view>
<view v-for="item in orders" :key="item.id || item.order_no" class="order">
<view class="order-main">
<view class="order-title">{{ item.title || item.subject || '订单' }}</view>
@ -13,7 +17,7 @@
</view>
<view class="order-right">
<view class="order-amount">{{ formatAmount(item.total_amount || item.amount || item.price) }}</view>
<view class="order-status">{{ statusText(item) }}</view>
<view class="order-status" :class="getStatusClass(item)">{{ statusText(item) }}</view>
</view>
</view>
<view v-if="loadingMore" class="loading">加载中...</view>
@ -65,6 +69,13 @@ function statusText(item) {
return s || ''
}
function getStatusClass(item) {
const text = statusText(item)
if (text === '待付款') return 'status-pending'
if (text === '已完成') return 'status-completed'
return ''
}
function switchTab(tab) {
if (currentTab.value === tab) return
currentTab.value = tab
@ -163,20 +174,175 @@ onReachBottom(() => {
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.tabs { display: flex; background: #fff; border-radius: 12rpx; padding: 8rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.tab { flex: 1; text-align: center; padding: 16rpx 0; font-size: 28rpx; color: #666 }
.tab.active { color: #007AFF; font-weight: 600 }
.order { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.order-main { display: flex; flex-direction: column }
.order-title { font-size: 28rpx; color: #333 }
.order-sub { font-size: 24rpx; color: #999; margin-top: 6rpx }
.order-right { display: flex; flex-direction: column; align-items: flex-end }
.order-amount { font-size: 28rpx; color: #333 }
.order-status { font-size: 24rpx; color: #666; margin-top: 6rpx }
.empty { text-align: center; color: #999; margin-top: 40rpx }
.error { color: #e43; margin-bottom: 12rpx }
.loading { text-align: center; color: #666; margin: 20rpx 0 }
.end { text-align: center; color: #999; margin: 20rpx 0 }
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 订单页面
采用暖橙色调的订单列表设计
============================================ */
.wrap {
padding: $spacing-md;
min-height: 100vh;
background-color: $bg-page;
position: relative;
overflow-x: hidden;
&::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100%; height: 100vh;
background: radial-gradient(circle at 10% 10%, rgba($brand-primary, 0.05), transparent 40%),
radial-gradient(circle at 90% 90%, rgba($accent-gold, 0.05), transparent 40%);
pointer-events: none;
z-index: 0;
}
}
/* Tab 切换 */
.tabs {
display: flex;
background: $bg-glass;
backdrop-filter: blur(10rpx);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: $radius-lg;
padding: 8rpx;
margin-bottom: $spacing-lg;
box-shadow: $shadow-sm;
}
.tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: $font-md;
color: $text-sub;
border-radius: $radius-md;
transition: all 0.25s ease;
font-weight: 500;
}
.tab.active {
background: $gradient-brand;
color: $text-inverse;
font-weight: 600;
box-shadow: $shadow-glow;
}
/* 订单卡片 */
.order {
display: flex;
justify-content: space-between;
align-items: center;
background: $bg-card;
border-radius: $radius-md;
padding: $spacing-lg;
margin-bottom: $spacing-lg;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.02);
transition: all 0.2s ease;
animation: fadeInUp 0.4s ease-out backwards;
}
@for $i from 1 through 10 {
.order:nth-child(#{$i}) {
animation-delay: #{$i * 0.05}s;
}
}
.order:active {
transform: scale(0.98);
box-shadow: none;
}
.order-main {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.order-title {
font-size: $font-md;
font-weight: 700;
color: $text-main;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: $spacing-xs;
}
.order-sub {
font-size: $font-sm;
color: $text-sub;
}
.order-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: $spacing-lg;
flex-shrink: 0;
}
.order-amount {
font-size: $font-lg;
font-weight: 800;
color: $brand-primary;
font-family: 'DIN Alternate', sans-serif;
}
.order-status {
font-size: $font-xs;
color: $text-sub;
margin-top: 10rpx;
padding: 4rpx $spacing-md;
background: $bg-page;
border-radius: $radius-round;
font-weight: 600;
&.status-pending {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
&.status-completed {
background: rgba($uni-color-success, 0.1);
color: $uni-color-success;
}
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 120rpx;
animation: fadeInUp 0.5s ease-out;
.empty-icon {
font-size: 80rpx;
margin-bottom: $spacing-lg;
opacity: 0.6;
animation: float 4s ease-in-out infinite;
}
.empty-text {
color: $text-sub;
font-size: $font-md;
}
}
/* 错误提示 */
.error {
color: $uni-color-error;
font-size: $font-sm;
margin-bottom: $spacing-md;
padding: $spacing-md;
background: rgba($uni-color-error, 0.1);
border-radius: $radius-lg;
text-align: center;
}
/* 加载状态 */
.loading, .end {
text-align: center;
color: $text-sub;
padding: $spacing-lg 0;
font-size: $font-sm;
}
</style>

View File

@ -1,18 +1,55 @@
<template>
<view class="wrap">
<view v-if="error" class="error">{{ error }}</view>
<view v-if="records.length === 0 && !loading" class="empty">暂无积分记录</view>
<view v-for="item in records" :key="item.id || item.time || item.created_at" class="record">
<view class="record-main">
<view class="record-title">{{ item.title || item.reason || '变更' }}</view>
<view class="record-time">{{ formatTime(item.time || item.created_at) }}</view>
</view>
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
</view>
<!-- 顶部装饰背景 -->
<view class="page-bg-decoration"></view>
<view class="header-area">
<view class="page-title">积分明细</view>
<view class="page-subtitle">Points Record</view>
</view>
<view class="content-area">
<view v-if="error" class="error-card">
<text class="error-icon"></text>
<text>{{ error }}</text>
</view>
<view v-if="records.length === 0 && !loading" class="empty-state">
<image class="empty-img" src="/static/empty-points.png" mode="widthFix" />
<text class="empty-text">暂无积分记录</text>
</view>
<view class="records-list" v-else>
<view
v-for="(item, index) in records"
:key="item.id || item.time || item.created_at"
class="record-item"
:style="{ animationDelay: `${index * 0.05}s` }"
>
<view class="record-icon" :class="{ 'is-add': (item.change || item.amount || 0) > 0 }">
{{ (item.change || item.amount || 0) > 0 ? '↓' : '↑' }}
</view>
<view class="record-content">
<view class="record-main">
<view class="record-title">{{ item.title || item.reason || '积分变更' }}</view>
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
</view>
</view>
<view class="record-footer">
<view class="record-time">{{ formatTime(item.time || item.created_at) }}</view>
<view class="record-status">交易成功</view>
</view>
</view>
</view>
</view>
<view v-if="loadingMore" class="loading-more">
<view class="spinner"></view>
<text>加载中...</text>
</view>
<view v-else-if="!hasMore && records.length > 0" class="no-more">- 到底啦 -</view>
</view>
<view v-if="loadingMore" class="loading">加载中...</view>
<view v-else-if="!hasMore && records.length > 0" class="end">没有更多了</view>
</view>
</template>
@ -101,17 +138,202 @@ onReachBottom(() => {
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.record { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.record-main { display: flex; flex-direction: column }
.record-title { font-size: 28rpx; color: #333 }
.record-time { font-size: 24rpx; color: #999; margin-top: 6rpx }
.record-amount { font-size: 32rpx }
.record-amount.inc { color: #18a058 }
.record-amount.dec { color: #d03050 }
.empty { text-align: center; color: #999; margin-top: 40rpx }
.error { color: #e43; margin-bottom: 12rpx }
.loading { text-align: center; color: #666; margin: 20rpx 0 }
.end { text-align: center; color: #999; margin: 20rpx 0 }
<style lang="scss" scoped>
.wrap {
min-height: 100vh;
background-color: $bg-page;
position: relative;
overflow-x: hidden;
}
.page-bg-decoration {
position: absolute;
top: -200rpx;
right: -200rpx;
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.15), transparent 70%);
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.header-area {
padding: $spacing-xl $spacing-lg;
position: relative;
z-index: 1;
}
.page-title {
font-size: 48rpx;
font-weight: 900;
color: $text-main;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.page-subtitle {
font-size: 24rpx;
color: $text-tertiary;
text-transform: uppercase;
letter-spacing: 2rpx;
font-weight: 600;
}
.content-area {
padding: 0 $spacing-lg $spacing-xl;
position: relative;
z-index: 1;
}
.records-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.record-item {
display: flex;
align-items: center;
background: rgba($bg-card, 0.8);
border-radius: $radius-lg;
padding: 30rpx;
box-shadow: $shadow-sm;
backdrop-filter: blur(10px);
border: 1rpx solid rgba(255,255,255,0.5);
animation: fadeInUp 0.5s ease-out backwards;
transition: all 0.2s;
&:active {
transform: scale(0.98);
background: rgba($bg-card, 0.95);
}
}
.record-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: $bg-secondary;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $text-secondary;
margin-right: 24rpx;
flex-shrink: 0;
font-weight: 800;
&.is-add {
background: rgba($uni-color-success, 0.1);
color: $uni-color-success;
}
}
.record-content {
flex: 1;
}
.record-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.record-title {
font-size: 30rpx;
font-weight: 700;
color: $text-main;
}
.record-amount {
font-size: 36rpx;
font-weight: 900;
font-family: 'DIN Alternate', sans-serif;
&.inc { color: $uni-color-success; }
&.dec { color: $text-main; }
}
.record-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.record-time {
font-size: 24rpx;
color: $text-tertiary;
}
.record-status {
font-size: 20rpx;
color: $text-tertiary;
background: $bg-secondary;
padding: 2rpx 10rpx;
border-radius: 6rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
.empty-img {
width: 240rpx;
margin-bottom: 30rpx;
opacity: 0.6;
filter: grayscale(100%);
}
.empty-text {
color: $text-tertiary;
font-size: 28rpx;
}
}
.error-card {
background: rgba($uni-color-error, 0.05);
border: 1rpx solid rgba($uni-color-error, 0.1);
color: $uni-color-error;
padding: 20rpx;
border-radius: $radius-md;
margin-bottom: 30rpx;
display: flex;
align-items: center;
font-size: 26rpx;
.error-icon {
margin-right: 12rpx;
}
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
color: $text-tertiary;
font-size: 24rpx;
gap: 12rpx;
}
.spinner {
width: 28rpx;
height: 28rpx;
border: 3rpx solid $bg-secondary;
border-top-color: $text-tertiary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.no-more {
text-align: center;
padding: 40rpx 0;
color: $text-tertiary;
font-size: 24rpx;
opacity: 0.6;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>

156
pages/register/register.vue Normal file
View File

@ -0,0 +1,156 @@
<template>
<view class="container">
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
<view class="title">注册新账号</view>
<view class="form">
<view class="input-row">
<text class="label">账号</text>
<input type="text" v-model="account" class="input-field" placeholder="请输入账号" />
</view>
<view class="input-row">
<text class="label">密码</text>
<input type="password" v-model="password" class="input-field" placeholder="请输入密码" />
</view>
<view class="input-row">
<text class="label">确认密码</text>
<input type="password" v-model="confirmPassword" class="input-field" placeholder="请再次输入密码" />
</view>
<button class="btn submit-btn" :disabled="loading" @click="onRegister">注册</button>
</view>
<view class="login-link">
<text @tap="goLogin">已有账号去登录</text>
</view>
<view v-if="error" class="error">{{ error }}</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const account = ref('')
const password = ref('')
const confirmPassword = ref('')
const loading = ref(false)
const error = ref('')
function goLogin() {
uni.navigateBack()
}
function onRegister() {
if (!account.value || !password.value) {
uni.showToast({ title: '请填写完整', icon: 'none' })
return
}
if (password.value !== confirmPassword.value) {
uni.showToast({ title: '两次密码不一致', icon: 'none' })
return
}
// TODO: API
uni.showToast({ title: '注册功能开发中', icon: 'none' })
}
</script>
<style scoped>
/* ============================================
奇盒潮玩 - 注册页面
============================================ */
.container {
min-height: 100vh;
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(180deg, #FFF8F3 0%, #FFE8D1 50%, #FFDAB9 100%);
}
.logo {
width: 160rpx;
height: 160rpx;
margin-top: 80rpx;
margin-bottom: 32rpx;
border-radius: 32rpx;
box-shadow: 0 12rpx 36rpx rgba(255, 107, 53, 0.2);
}
.title {
font-size: 40rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 48rpx;
}
.form {
width: 100%;
max-width: 600rpx;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border-radius: 32rpx;
padding: 40rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.08);
}
.input-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
background: #F9FAFB;
border-radius: 16rpx;
padding: 8rpx 24rpx;
border: 2rpx solid #E5E7EB;
}
.label {
width: 140rpx;
font-size: 28rpx;
font-weight: 500;
color: #6B7280;
}
.input-field {
flex: 1;
height: 80rpx;
border: none;
background: transparent;
font-size: 28rpx;
color: #1F2937;
}
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
margin-top: 32rpx;
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
border: none;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
}
.submit-btn:active {
transform: scale(0.97);
}
.login-link {
margin-top: 32rpx;
font-size: 26rpx;
color: #FF9F43;
font-weight: 500;
}
.error {
margin-top: 24rpx;
color: #EF4444;
font-size: 26rpx;
text-align: center;
}
</style>

163
pages/shop/detail.vue Normal file
View File

@ -0,0 +1,163 @@
<template>
<view class="page">
<view class="bg-decoration"></view>
<view class="loading" v-if="loading">加载中...</view>
<view v-else-if="detail.id" class="detail-wrap">
<image v-if="detail.main_image" class="main-image" :src="detail.main_image" mode="widthFix" />
<view class="info-card">
<view class="title">{{ detail.title || detail.name || '-' }}</view>
<view class="price-row">
<text class="price">¥{{ formatPrice(detail.price_sale || detail.price) }}</text>
<text class="points" v-if="detail.points_required">{{ detail.points_required }}积分</text>
</view>
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存{{ detail.stock }}</view>
<view class="desc" v-if="detail.description">{{ detail.description }}</view>
</view>
</view>
<view v-else class="empty">商品不存在</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getProductDetail } from '../../api/appUser'
const detail = ref({})
const loading = ref(false)
function formatPrice(p) {
if (p === undefined || p === null) return '0.00'
return (Number(p) / 100).toFixed(2)
}
async function fetchDetail(id) {
loading.value = true
try {
const res = await getProductDetail(id)
detail.value = res || {}
} catch (e) {
detail.value = {}
} finally {
loading.value = false
}
}
onLoad((opts) => {
const id = opts && opts.id
if (id) fetchDetail(id)
})
</script>
<style lang="scss" scoped>
/* ============================================
奇盒潮玩 - 商品详情页
============================================ */
.page {
min-height: 100vh;
background: $bg-page;
padding-bottom: env(safe-area-inset-bottom);
}
.loading, .empty {
text-align: center;
padding: 120rpx 40rpx;
color: $text-secondary;
font-size: $font-md;
}
.detail-wrap {
padding-bottom: 40rpx;
animation: fadeInUp 0.4s ease-out;
}
.main-image {
width: 100%;
height: 750rpx; /* Square aspect ratio */
display: block;
background: $bg-secondary;
box-shadow: $shadow-sm;
}
.info-card {
margin: $spacing-lg;
margin-top: -60rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: $radius-xl;
padding: $spacing-xl;
box-shadow: $shadow-lg;
position: relative;
z-index: 2;
}
.title {
font-size: $font-xl;
font-weight: 800;
color: $text-main;
margin-bottom: $spacing-md;
line-height: 1.4;
}
.price-row {
display: flex;
align-items: baseline;
gap: $spacing-sm;
margin-bottom: $spacing-lg;
}
.price {
font-size: $font-xxl;
font-weight: 900;
color: $brand-primary;
font-family: 'DIN Alternate', sans-serif;
&::before {
content: '¥';
font-size: $font-md;
margin-right: 4rpx;
}
}
.points {
font-size: $font-sm;
color: $brand-primary;
padding: 6rpx $spacing-md;
background: rgba($brand-primary, 0.1);
border-radius: 100rpx;
font-weight: 600;
}
.stock {
font-size: $font-sm;
color: $text-secondary;
margin-bottom: $spacing-lg;
background: $bg-secondary;
display: inline-block;
padding: 6rpx $spacing-md;
border-radius: $radius-sm;
}
.desc {
font-size: $font-lg;
color: $text-main;
line-height: 1.8;
padding-top: $spacing-lg;
border-top: 1rpx dashed $border-color-light;
&::before {
content: '商品详情';
display: block;
font-size: $font-md;
color: $text-secondary;
margin-bottom: $spacing-sm;
font-weight: 700;
}
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40rpx); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -1,52 +1,60 @@
<template>
<view class="page">
<view v-if="showNotice" class="notice-mask" @touchmove.stop.prevent @tap.stop>
<view class="notice-dialog" @tap.stop>
<view class="notice-title">提示</view>
<view class="notice-content">由于价格浮动当前暂不支持自行兑换商品兑换请联系客服核对价格</view>
<view class="notice-actions">
<view class="notice-check" @tap.stop="toggleHideForever">
<view class="check-box" :class="{ on: hideForever }"></view>
<text class="check-text">不再显示</text>
</view>
<button class="notice-btn" type="primary" @tap.stop="onDismissNotice">我知道了</button>
<view class="bg-decoration"></view>
<!-- 顶部固定区域 -->
<view class="header-section">
<view class="search-box">
<view class="search-input-wrap">
<text class="search-icon">🔍</text>
<input class="search-input" v-model="keyword" placeholder="搜索心仪的商品" placeholder-class="placeholder-style" confirm-type="search" @confirm="onSearchConfirm" />
</view>
</view>
<view class="filter-row">
<view class="price-range">
<text class="price-label">价格区间</text>
<input class="price-input" type="number" v-model="minPrice" placeholder="最低" placeholder-class="price-ph" />
<text class="price-sep">-</text>
<input class="price-input" type="number" v-model="maxPrice" placeholder="最高" placeholder-class="price-ph" />
</view>
<button class="filter-btn" hover-class="btn-hover" @tap="onApplyFilters">筛选</button>
</view>
</view>
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
<view class="products-section" v-else>
<view class="section-title">商品</view>
<view class="toolbar">
<input class="search" v-model="keyword" placeholder="搜索商品" confirm-type="search" @confirm="onSearchConfirm" />
<view class="filters">
<input class="price" type="number" v-model="minPrice" placeholder="最低价" />
<text class="dash">-</text>
<input class="price" type="number" v-model="maxPrice" placeholder="最高价" />
<button class="apply-btn" size="mini" @tap="onApplyFilters">筛选</button>
</view>
</view>
<view v-if="displayCount" class="products-columns">
<view class="column" v-for="(col, ci) in columns" :key="ci">
<view class="product-item" v-for="p in col" :key="p.id" @tap="onProductTap(p)">
<!-- 占位防止内容被头部遮挡 -->
<view class="header-placeholder"></view>
<view v-if="loading && !products.length" class="loading-wrap"><view class="spinner"></view></view>
<view class="products-container" v-else>
<view v-if="products.length > 0" class="products-grid">
<view class="product-item" v-for="p in products" :key="p.id" @tap="onProductTap(p)">
<view class="product-card">
<view class="thumb-wrap">
<image class="product-thumb" :class="{ visible: isLoaded(p) }" :src="p.image" mode="widthFix" lazy-load="true" @load="onImageLoad(p)" @error="onImageError(p)" />
<view v-if="!isLoaded(p)" class="skeleton"></view>
<view class="badge">
<text class="badge-price" v-if="p.price !== null">{{ p.price }}</text>
<text class="badge-points" v-if="p.points !== null">{{ p.points }}积分</text>
</view>
<image class="product-thumb" :src="p.image" mode="aspectFill" lazy-load="true" />
<view class="stock-tag" v-if="p.stock !== null && p.stock < 10">仅剩{{p.stock}}</view>
</view>
<text class="product-title">{{ p.title }}</text>
<view class="product-extra" v-if="p.stock !== null">
<text class="stock">库存 {{ p.stock }}</text>
<view class="product-info">
<text class="product-title">{{ p.title }}</text>
<view class="product-bottom">
<view class="price-row">
<text class="price-symbol"></text>
<text class="price-val">{{ p.price }}</text>
</view>
<view class="points-badge" v-if="p.points">
<text class="points-val">{{ p.points }}</text>
<text class="points-unit">积分</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
<text class="empty-text">暂无相关商品</text>
</view>
</view>
<view v-else class="empty">暂无商品</view>
</view>
</view>
</template>
@ -56,98 +64,29 @@ import { ref, computed } from 'vue'
import { request, authRequest } from '../../utils/request.js'
const products = ref([])
const columns = ref([[], []])
const colHeights = ref([0, 0])
const CACHE_KEY = 'products_cache_v1'
const TTL_MS = 10 * 60 * 1000
const loading = ref(false)
const keyword = ref('')
const minPrice = ref('')
const maxPrice = ref('')
const displayCount = computed(() => (columns.value[0].length + columns.value[1].length))
const loadedMap = ref({})
const showNotice = ref(false)
const hideForever = ref(false)
const skipReloadOnce = ref(false)
function getKey(p) { return String((p && p.id) ?? '') + '|' + String((p && p.image) ?? '') }
function unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const data = obj.data || {}
const arr = obj.list || obj.items || data.list || data.items || data
return Array.isArray(arr) ? arr : []
}
function cleanUrl(u) {
const s = String(u || '').trim()
const m = s.match(/https?:\/\/[^\s'"`]+/)
if (m && m[0]) return m[0]
return s.replace(/[`'\"]/g, '').trim()
}
function apiGet(url, data = {}) {
const token = uni.getStorageSync('token')
const fn = token ? authRequest : request
return fn({ url, method: 'GET', data })
}
function getCachedProducts() {
try {
const obj = uni.getStorageSync(CACHE_KEY)
if (obj && Array.isArray(obj.data) && typeof obj.ts === 'number') {
const fresh = Date.now() - obj.ts < TTL_MS
if (fresh) return obj.data
}
} catch (_) {}
return null
}
function setCachedProducts(list) {
try {
uni.setStorageSync(CACHE_KEY, { data: Array.isArray(list) ? list : [], ts: Date.now() })
} catch (_) {}
}
function estimateHeight(p) {
const base = 220
const len = String(p.title || '').length
const lines = Math.min(2, Math.ceil(len / 12))
const titleH = lines * 36
const stockH = p.stock !== null && p.stock !== undefined ? 34 : 0
const padding = 28
return base + titleH + stockH + padding
}
function distributeToColumns(list) {
const arr = Array.isArray(list) ? list : []
const cols = Array.from({ length: 2 }, () => [])
const hs = [0, 0]
for (let i = 0; i < arr.length; i++) {
const h = estimateHeight(arr[i])
const idx = hs[0] <= hs[1] ? 0 : 1
cols[idx].push(arr[i])
hs[idx] += h
}
columns.value = cols
colHeights.value = hs
const presentKeys = new Set(arr.map(getKey))
const next = {}
const prev = loadedMap.value || {}
for (const k in prev) { if (presentKeys.has(k)) next[k] = prev[k] }
loadedMap.value = next
}
function extractListAndTotal(payload) {
if (Array.isArray(payload)) return { list: payload, total: payload.length }
const obj = payload || {}
const data = obj.data || {}
const list = obj.list || obj.items || data.list || data.items || []
const totalRaw = obj.total ?? data.total
const total = typeof totalRaw === 'number' ? totalRaw : (Array.isArray(list) ? list.length : 0)
return { list: Array.isArray(list) ? list : [], total }
function cleanUrl(u) {
const s = String(u || '').trim()
const m = s.match(/https?:\/\/[^\s'"`]+/)
if (m && m[0]) return m[0]
return s.replace(/[`'\"]/g, '').trim()
}
function normalizeProducts(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
if (!Array.isArray(list)) return []
return list.map((i, idx) => ({
id: i.id ?? i.productId ?? i._id ?? i.sku_id ?? String(idx),
image: cleanUrl(i.main_image ?? i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
title: i.title ?? i.name ?? i.product_name ?? i.sku_name ?? '',
@ -169,14 +108,18 @@ function onProductTap(p) {
}
}
// Filter logic
const allProducts = ref([]) // Store all fetched products for client-side filtering
function applyFilters() {
const k = String(keyword.value || '').trim().toLowerCase()
const min = Number(minPrice.value)
const max = Number(maxPrice.value)
const hasMin = !isNaN(min) && String(minPrice.value).trim() !== ''
const hasMax = !isNaN(max) && String(maxPrice.value).trim() !== ''
const list = Array.isArray(products.value) ? products.value : []
const filtered = list.filter(p => {
const list = allProducts.value
products.value = list.filter(p => {
const title = String(p.title || '').toLowerCase()
if (k && !title.includes(k)) return false
const priceNum = typeof p.price === 'number' ? p.price : Number(p.price)
@ -190,7 +133,6 @@ function applyFilters() {
}
return true
})
distributeToColumns(filtered)
}
function onSearchConfirm() { applyFilters() }
@ -198,56 +140,50 @@ function onApplyFilters() { applyFilters() }
async function loadProducts() {
try {
const cached = getCachedProducts()
if (cached) {
products.value = cached
distributeToColumns(cached)
return
const cached = uni.getStorageSync(CACHE_KEY)
if (cached && cached.data && Date.now() - cached.ts < TTL_MS) {
allProducts.value = cached.data
applyFilters()
return
}
const first = await apiGet('/api/app/products', { page: 1 })
const { list: firstList, total } = extractListAndTotal(first)
// Simple extraction
let list = []
let total = 0
if (first && first.list) { list = first.list; total = first.total }
else if (first && first.data && first.data.list) { list = first.data.list; total = first.data.total }
// If not too many, fetch all for better client UX
const pageSize = 20
const totalPages = Math.max(1, Math.ceil(((typeof total === 'number' ? total : 0)) / pageSize))
if (totalPages <= 1) {
const normalized = normalizeProducts(firstList)
products.value = normalized
distributeToColumns(normalized)
setCachedProducts(normalized)
return
const totalPages = Math.ceil((total || 0) / pageSize)
if (totalPages > 1) {
const tasks = []
for (let p = 2; p <= totalPages; p++) {
tasks.push(apiGet('/api/app/products', { page: p }))
}
const results = await Promise.allSettled(tasks)
results.forEach(r => {
if (r.status === 'fulfilled') {
const val = r.value
const subList = (val && val.list) || (val && val.data && val.data.list) || []
if (Array.isArray(subList)) list = list.concat(subList)
}
})
}
const tasks = []
for (let p = 2; p <= totalPages; p++) {
tasks.push(apiGet('/api/app/products', { page: p }))
}
const results = await Promise.allSettled(tasks)
const restLists = results.map(r => {
if (r.status === 'fulfilled') {
const { list } = extractListAndTotal(r.value)
return Array.isArray(list) ? list : []
}
return []
})
const merged = [firstList, ...restLists].flat()
const normalized = normalizeProducts(merged)
products.value = normalized
distributeToColumns(normalized)
setCachedProducts(normalized)
const normalized = normalizeProducts(list)
allProducts.value = normalized
applyFilters()
uni.setStorageSync(CACHE_KEY, { data: normalized, ts: Date.now() })
} catch (e) {
console.error(e)
products.value = []
columns.value = [[], []]
colHeights.value = [0, 0]
const presentKeys = new Set([])
const next = {}
const prev = loadedMap.value || {}
for (const k in prev) { if (presentKeys.has(k)) next[k] = prev[k] }
loadedMap.value = next
}
}
function isLoaded(p) { return !!(loadedMap.value && loadedMap.value[getKey(p)]) }
function onImageLoad(p) { const k = getKey(p); if (!k) return; loadedMap.value = { ...(loadedMap.value || {}), [k]: true } }
function onImageError(p) { const k = getKey(p); if (!k) return; const prev = { ...(loadedMap.value || {}) }; delete prev[k]; loadedMap.value = prev }
onShow(async () => {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
@ -264,77 +200,353 @@ onShow(async () => {
})
return
}
try {
const sess = String(uni.getStorageSync('app_session_id') || '')
const hiddenSess = String(uni.getStorageSync('shop_notice_hidden_session_id') || '')
const hiddenThisSession = !!(sess && hiddenSess && hiddenSess === sess)
showNotice.value = !hiddenThisSession
hideForever.value = hiddenThisSession
} catch (_) { showNotice.value = true; hideForever.value = false }
try {
const skip = !!uni.getStorageSync('shop_skip_reload_once')
if (skipReloadOnce.value || skip) {
skipReloadOnce.value = false
uni.setStorageSync('shop_skip_reload_once', '')
return
}
} catch (_) {}
loading.value = true
await loadProducts()
loading.value = false
})
function toggleHideForever() { hideForever.value = !hideForever.value }
function onDismissNotice() {
if (hideForever.value) {
try {
const sess = String(uni.getStorageSync('app_session_id') || '')
if (sess) uni.setStorageSync('shop_notice_hidden_session_id', sess)
} catch (_) {}
//
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
onShareAppMessage(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '奇盒潮玩商城 - 好物等你来兑',
path: `/pages/index/index?invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
showNotice.value = false
}
})
onShareTimeline(() => {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '奇盒潮玩商城 - 好物等你来兑',
query: `invite_code=${inviteCode}`,
imageUrl: '/static/logo.png'
}
})
</script>
<style scoped>
.page { padding: 24rpx }
.notice-mask { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); z-index: 9999; display: flex; align-items: center; justify-content: center }
.notice-dialog { width: 86%; max-width: 640rpx; background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.18) }
.notice-title { font-size: 32rpx; font-weight: 600; padding: 24rpx 24rpx 0 }
.notice-content { padding: 16rpx 24rpx; font-size: 26rpx; color: #333; line-height: 1.6 }
.notice-actions { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx 24rpx }
.notice-check { display: flex; align-items: center; gap: 8rpx }
.check-box { width: 28rpx; height: 28rpx; border-radius: 6rpx; border: 2rpx solid #007AFF; background: #fff }
.check-box.on { background: #007AFF }
.check-text { font-size: 26rpx; color: #555 }
.notice-btn { background: #007AFF; color: #fff; border-radius: 999rpx; padding: 0 28rpx }
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
.products-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx; margin-top: 24rpx }
.loading-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center }
.spinner { width: 56rpx; height: 56rpx; border: 6rpx solid rgba(0,122,255,0.15); border-top-color: #007AFF; border-radius: 50%; animation: spin 1s linear infinite }
@keyframes spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
.toolbar { display: flex; flex-direction: column; gap: 12rpx; margin-bottom: 16rpx }
.search { background: #f6f8ff; border: 1rpx solid rgba(0,122,255,0.25); border-radius: 999rpx; padding: 14rpx 20rpx; font-size: 26rpx }
.filters { display: flex; align-items: center; gap: 12rpx }
.price { flex: 1; background: #f6f8ff; border: 1rpx solid rgba(0,122,255,0.25); border-radius: 999rpx; padding: 12rpx 16rpx; font-size: 26rpx }
.dash { color: #888; font-size: 26rpx }
.apply-btn { background: #007AFF; color: #fff; border-radius: 999rpx; padding: 0 20rpx }
.products-columns { display: flex; gap: 12rpx }
.column { flex: 1 }
.product-item { margin-bottom: 12rpx }
.empty { padding: 40rpx; color: #888; text-align: center }
.product-card { background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,122,255,0.08); transition: transform .15s ease }
.product-item:active .product-card { transform: scale(0.98) }
.thumb-wrap { position: relative }
.product-thumb { width: 100%; height: auto; display: block; opacity: 0; transition: opacity .25s ease; z-index: 0 }
.product-thumb.visible { opacity: 1 }
.skeleton { position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: linear-gradient(90deg, #eef2ff 25%, #f6f8ff 37%, #eef2ff 63%); background-size: 400% 100%; animation: shimmer 1.2s ease infinite; z-index: 1 }
@keyframes shimmer { 0% { background-position: 100% 0 } 100% { background-position: 0 0 } }
.thumb-wrap { background: #f6f8ff; min-height: 220rpx }
.badge { position: absolute; left: 12rpx; bottom: 12rpx; display: flex; gap: 8rpx }
.badge-price { background: #007AFF; color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; box-shadow: 0 2rpx 8rpx rgba(0,122,255,0.25) }
.badge-points { background: rgba(0,122,255,0.85); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx }
.product-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin: 12rpx; font-size: 26rpx; color: #222 }
.product-extra { display: flex; justify-content: flex-end; align-items: center; margin: 0 12rpx 12rpx }
.stock { font-size: 22rpx; color: #888 }
<style lang="scss" scoped>
.page {
min-height: 100vh;
background-color: $bg-page;
padding-bottom: 40rpx;
position: relative;
overflow-x: hidden;
}
.bg-decoration {
position: fixed;
top: 0; left: 0; width: 100%; height: 100vh;
pointer-events: none;
z-index: 0;
&::before {
content: '';
position: absolute;
top: -100rpx; left: -100rpx;
width: 600rpx; height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.1) 0%, transparent 70%);
filter: blur(60rpx);
border-radius: 50%;
opacity: 0.6;
animation: float 10s ease-in-out infinite;
}
&::after {
content: '';
position: absolute;
bottom: 10%; right: -100rpx;
width: 500rpx; height: 500rpx;
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
filter: blur(50rpx);
border-radius: 50%;
opacity: 0.5;
animation: float 15s ease-in-out infinite reverse;
}
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 50rpx); }
}
/* 顶部 Header */
.header-section {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: $bg-glass;
backdrop-filter: blur(20rpx);
padding: 0 24rpx 24rpx;
box-shadow: $shadow-sm;
border-bottom: 1rpx solid $border-color-light;
}
.header-placeholder {
height: 160rpx; /* 根据 header 高度调整 */
}
.page-title {
font-size: 36rpx;
font-weight: 800;
color: $text-main;
padding: 20rpx 0;
}
/* 搜索框 */
.search-box {
margin-bottom: 20rpx;
margin-top: 20rpx;
}
.search-input-wrap {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: $radius-round;
padding: 18rpx 24rpx;
transition: all 0.3s;
}
.search-input-wrap:focus-within {
background: $bg-card;
border-color: $brand-primary;
box-shadow: 0 0 0 4rpx rgba($brand-primary, 0.1);
}
.search-icon {
font-size: 28rpx;
margin-right: 16rpx;
opacity: 0.5;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: $text-main;
}
.placeholder-style {
color: $text-tertiary;
}
/* 筛选行 */
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.price-range {
flex: 1;
display: flex;
align-items: center;
background: $bg-secondary;
border-radius: $radius-md;
padding: 10rpx 20rpx;
}
.price-label {
font-size: 24rpx;
color: $text-sub;
margin-right: 16rpx;
}
.price-input {
flex: 1;
font-size: 26rpx;
text-align: center;
color: $text-main;
}
.price-ph {
color: $text-tertiary;
font-size: 24rpx;
}
.price-sep {
color: $text-tertiary;
margin: 0 10rpx;
}
.filter-btn {
background: $gradient-brand;
color: $text-inverse;
font-size: 26rpx;
font-weight: 600;
border-radius: $radius-md;
padding: 0 32rpx;
height: 64rpx;
line-height: 64rpx;
border: none;
box-shadow: $shadow-sm;
}
.btn-hover {
opacity: 0.9;
transform: scale(0.98);
}
/* 商品 Grid 容器 */
.products-container {
padding: 24rpx;
}
.products-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
/* 商品卡片 */
.product-item {
animation: fadeInUp 0.5s ease-out backwards;
}
@for $i from 1 through 10 {
.product-item:nth-child(#{$i}) {
animation-delay: #{$i * 0.05}s;
}
}
.product-card {
background: $bg-card;
border-radius: $radius-lg;
overflow: hidden;
box-shadow: $shadow-card;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
.product-item:active .product-card {
transform: scale(0.98);
box-shadow: $shadow-sm;
}
.thumb-wrap {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
background: $bg-secondary;
}
.product-thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.stock-tag {
position: absolute;
bottom: 0;
right: 0;
background: rgba($accent-red, 0.9);
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-top-left-radius: 12rpx;
backdrop-filter: blur(4px);
font-weight: 700;
box-shadow: 0 -2rpx 8rpx rgba(0,0,0,0.1);
}
.product-info {
padding: 20rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-title {
font-size: 28rpx;
color: $text-main;
line-height: 1.4;
height: 2.8em; /* 2 lines */
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 16rpx;
font-weight: 600;
}
.product-bottom {
display: flex;
align-items: flex-end;
justify-content: space-between;
flex-wrap: wrap;
gap: 8rpx;
}
.price-row {
color: $accent-red;
font-weight: 700;
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 24rpx;
}
.price-val {
font-size: 34rpx;
}
.points-badge {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
border: 1px solid rgba($brand-primary, 0.2);
border-radius: 8rpx;
padding: 2rpx 10rpx;
display: flex;
align-items: center;
gap: 4rpx;
}
.points-val {
font-size: 24rpx;
font-weight: 700;
}
.points-unit {
font-size: 20rpx;
}
/* Loading & Empty */
.loading-wrap {
padding: 100rpx 0;
display: flex;
justify-content: center;
}
.spinner {
width: 50rpx;
height: 50rpx;
border: 4rpx solid rgba($brand-primary, 0.2);
border-top-color: $brand-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 100rpx;
}
.empty-img {
width: 240rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
color: $text-tertiary;
font-size: 28rpx;
}
.check-text {
font-size: 26rpx;
color: $text-sub;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

25
project.config.json Normal file
View File

@ -0,0 +1,25 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wx26ad074017e1e63f",
"editorSetting": {}
}

View File

@ -0,0 +1,14 @@
{
"libVersion": "3.12.1",
"projectname": "bindbox-mini",
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
}
}

211
uni.scss
View File

@ -1,76 +1,185 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
* 奇盒潮玩 - 全局样式系统
*
* 基于潮玩盲盒风格的设计系统采用暖橙渐变色调
* 传递惊喜期待活力的品牌调性
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
/* ============================================
🎨 品牌色彩系统 - 潮玩盲盒 Pro (2025 Refined)
============================================ */
/* 颜色变量 */
/* 主色调 - 更加活力与现代 */
$brand-primary: #FF6B00; // 核心品牌橙 (更纯粹)
$brand-secondary: #FF9500; // 次级品牌色 (用于渐变/辅助)
$brand-primary-light: #FF9500; // 亮橙
$brand-primary-dark: #E65100; // 深橙
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 辅助色 - 丰富视觉层次 */
$accent-gold: #FFC107; // 质感金
$accent-red: #FF3B30; // 促销红
$accent-blue: #007AFF; // 科技蓝
$accent-purple: #AF52DE; // 梦幻紫
$accent-pink: #FF2D55; // 活力粉
$accent-cyan: #5AC8FA; // 清新蓝
$color-success: #34C759; // 成功色
$color-warning: #FF9F0A; // 警告色
$color-error: #FF3B30; // 错误色
/* 中性色 - 提升阅读体验 */
$text-main: #1D1D1F; // 主要文字 (接近纯黑但柔和)
$text-sub: #86868B; // 次要文字
$text-secondary: $text-sub; // Alias for compatibility
$text-tertiary: #C7C7CC; // 辅助/占位
$text-placeholder: $text-tertiary; // Alias for compatibility
$text-disabled: #D1D1D6; // 禁用状态文字
$text-inverse: #FFFFFF; // 反白文字
/* 背景色 - 营造氛围 */
$bg-page: #F5F5F7; // 页面底色 (高级灰)
$bg-card: #FFFFFF; // 卡片背景
$bg-glass: rgba(255, 255, 255, 0.8); // 毛玻璃背景
$bg-secondary: #F8F8F8; // 次级背景
$bg-grey: #FAFAFA; // 浅灰背景
/* 渐变色 - 视觉冲击力 */
// 使用 CSS 变量在 style 中定义此处保留 SCSS 变量供编译使用
$gradient-brand: linear-gradient(135deg, #FF9500 0%, #FF5E00 100%);
$gradient-gold: linear-gradient(135deg, #FFD60A 0%, #FF9F0A 100%);
$gradient-red: linear-gradient(135deg, #FF3B30 0%, #D32F2F 100%);
$gradient-purple: linear-gradient(135deg, #BF5AF2 0%, #5E5CE6 100%);
/* ============================================
🌑 暗黑/特殊主题变量 (Dark Mode Support)
============================================ */
$bg-dark: #1A1A2E; // 深邃蓝黑
$bg-dark-card: rgba(30, 30, 50, 0.7); // 暗色玻璃卡片
$text-dark-main: #FFFFFF; // 暗色模式主字
$text-dark-sub: rgba(255, 255, 255, 0.7); // 暗色模式副字
$border-dark: rgba(255, 255, 255, 0.1); // 暗色边框
/* ============================================
📐 布局与间距
============================================ */
$font-xxs: 18rpx;
$font-xs: 22rpx;
$font-sm: 24rpx;
$font-md: 28rpx;
$font-lg: 32rpx;
$font-xl: 36rpx;
$font-xxl: 48rpx;
$spacing-xs: 8rpx;
$spacing-sm: 12rpx;
$spacing-md: 16rpx;
$spacing-lg: 24rpx;
$spacing-xl: 32rpx;
$spacing-xxl: 48rpx;
$radius-sm: 8rpx;
$radius-md: 16rpx;
$radius-lg: 24rpx;
$radius-xl: 32rpx;
$radius-round: 999rpx;
$border-color-light: #F0F0F0;
$border-color: $border-color-light;
/* ============================================
阴影与质感
============================================ */
$shadow-xs: 0 1rpx 4rpx rgba(0, 0, 0, 0.02);
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
$shadow-md: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
$shadow-card: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
$shadow-float: 0 16rpx 48rpx rgba(255, 107, 0, 0.15); // 悬浮投影
$shadow-inner: inset 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
$shadow-lg: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
$shadow-warm: 0 8rpx 24rpx rgba(255, 107, 53, 0.15);
$shadow-glow: 0 4rpx 16rpx rgba(255, 159, 67, 0.4);
/* ============================================
🔄 动画时长
============================================ */
$transition-fast: 0.15s;
$transition-normal: 0.25s;
$transition-slow: 0.35s;
$ease-out: cubic-bezier(0.4, 0, 0.2, 1);
$ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* ============================================
📱 兼容uni-app官方变量
============================================ */
/* 行为相关颜色 - 使用新配色 */
$uni-color-primary: $brand-primary;
$uni-color-success: #10B981;
$uni-color-warning: $accent-gold;
$uni-color-error: $accent-red;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
$uni-text-color: $text-main;
$uni-text-color-inverse: $text-inverse;
$uni-text-color-grey: $text-tertiary;
$uni-text-color-placeholder: $text-tertiary;
$uni-text-color-disable: #D1D5DB;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
$uni-bg-color: $bg-card;
$uni-bg-color-grey: $bg-page;
$uni-bg-color-hover: #FFF0E6;
$uni-bg-color-mask: rgba(0, 0, 0, 0.5);
/* 边框颜色 */
$uni-border-color:#c8c7cc;
$uni-border-color: #E5E7EB;
/* 尺寸变量 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16px;
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-sm: 4px;
$uni-border-radius-base: 8px;
$uni-border-radius-lg: 12px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
$uni-spacing-row-sm: 6px;
$uni-spacing-row-base: 12px;
$uni-spacing-row-lg: 16px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
$uni-spacing-col-sm: 6px;
$uni-spacing-col-base: 10px;
$uni-spacing-col-lg: 14px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
$uni-opacity-disabled: 0.4;
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;
$uni-color-title: $text-main;
$uni-font-size-title: 20px;
$uni-color-subtitle: $text-sub;
$uni-font-size-subtitle: 16px;
$uni-color-paragraph: $text-sub;
$uni-font-size-paragraph: 15px;
/* ============================================
🛠 工具 Mixins
============================================ */
@mixin text-ellipsis($lines: 1) {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
}

34
说明文档.md Normal file
View File

@ -0,0 +1,34 @@
# 说明文档
## 一、项目规划
### 1.1 项目概述
本项目为 Uni-App 开发的 bindbox-mini 小程序。当前阶段任务为优化现有 UI 设计,重点调整色彩、按钮样式及视觉层级,保持整体布局不变。
### 1.2 实施方案
1. **全局样式分析**:检查 `uni.scss` 及各页面样式,提取现有色彩和组件样式。
2. **视觉优化设计**
* 定义新的色彩系统(主色、辅助色、中性色)。
* 优化按钮样式(圆角、阴影、交互态)。
* 提升卡片及列表的视觉质感。
3. **代码实现**
* 更新 `uni.scss` 变量。
* 重构主要组件/页面的样式代码。
4. **验证与测试**:确保各页面视觉一致且无布局崩坏。
### 1.3 进度记录
* [x] 2025-12-17: 初始化说明文档,启动 UI 优化任务。
* [x] 2025-12-17: 完成全局 SCSS 变量定义 (`uni.scss`),建立色彩与设计系统。
* [x] 2025-12-17: 优化 `pages/index/index.vue` (首页),集成新设计系统与微交互动画。
* [x] 2025-12-17: 优化 `pages/activity/pata/index.vue` (帕塔页面),增强动画与视觉质感。
* [x] 2025-12-17: 优化 `pages/activity/wuxianshang/index.vue` (无限赏),统一视觉风格。
* [x] 2025-12-17: 优化 `pages/mine/index.vue` (个人中心),实现磨砂玻璃效果与动态交互。
* [x] 2025-12-17: 优化 `pages/orders/index.vue` (订单列表),增强状态标识与列表质感。
* [x] 2025-12-17: 修复 `pages/address` 模块编译错误,并完成地址管理页面的样式标准化优化。
* [x] 2025-12-17: 修复 `components/ElCard.vue``components/YifanSelector.vue` 中的未定义变量错误,统一组件样式。
* [x] 2025-12-17: 优化 `pages/activity/wuxianshang/index.vue` (无限赏),替换硬编码样式为全局变量。
* [x] 2025-12-17: 修复 `pages/activity/yifanshang/index.vue` 编译错误,在 `uni.scss` 中补充 `text-ellipsis` mixin 定义。
* [x] 2025-12-17: 修复 `pages/login/index.vue` 等多处 `$border-color` 未定义错误,在 `uni.scss` 中增加变量别名。
* [x] 2025-12-17: 修复 `pages/mine/index.vue` 编译错误,在 `api/appUser.js` 中补充 `getUserInfo`, `getUserTasks`, `getInviteRecords` 导出。
* [ ] 2025-12-17: 进行中 - 优化 `pages/activity/yifanshang/index.vue` 及相关组件。
* [ ] 2025-12-17: 待开始 - 优化 `pages/login/index.vue` 视觉细节。