feat: Overhaul homepage UI with new activity sections and add dedicated activity, shop, and registration pages.

This commit is contained in:
邹方成 2025-12-15 20:16:47 +08:00
parent 5298ed1acf
commit 8a6ac48d59
22 changed files with 4118 additions and 1582 deletions

View File

@ -65,19 +65,132 @@ 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 }
/* ============================================
奇盒潮玩 - 翻牌动画组件
采用暖橙色调的开箱效果
============================================ */
.flip-root {
display: flex;
flex-direction: column;
gap: 20rpx;
padding: 20rpx;
}
.flip-actions {
display: flex;
gap: 16rpx;
}
.flip-btn {
flex: 1;
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
border-radius: 16rpx;
font-weight: 600;
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.35);
}
.flip-btn:active {
transform: scale(0.97);
}
.flip-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10rpx);
border-radius: 24rpx;
padding: 16rpx;
}
.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: 16rpx;
overflow: hidden;
}
.flip-front {
background: linear-gradient(145deg, #FFF8F3, #FFE8D1);
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(255, 159, 67, 0.2);
box-shadow: 0 4rpx 12rpx rgba(255, 159, 67, 0.15);
}
.front-placeholder {
width: 60%;
height: 60%;
border-radius: 16rpx;
background: linear-gradient(135deg, rgba(255, 159, 67, 0.3), rgba(255, 107, 53, 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: #FFFFFF;
transform: rotateY(180deg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16rpx;
border: 2rpx solid rgba(255, 159, 67, 0.3);
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.2);
}
.flip-image {
width: 75%;
border-radius: 12rpx;
margin-bottom: 8rpx;
background: linear-gradient(145deg, #FFF8F3, #FFF4E6);
}
.flip-title {
font-size: 24rpx;
font-weight: 600;
color: #1F2937;
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, #FFD166, #FF9F43) !important;
color: #6b4b1f !important;
border-radius: 999rpx;
font-weight: 600;
box-shadow: 0 6rpx 16rpx rgba(255, 159, 67, 0.35);
}
.flip-reset:active {
transform: scale(0.96);
}
</style>

View File

@ -134,13 +134,18 @@ function handleConfirm() {
</script>
<style 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: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
padding: 32rpx;
padding-bottom: calc(32rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(32rpx + 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;
background: linear-gradient(135deg, #FFF8F3, #FFF4E6);
color: #B45309;
font-size: 24rpx;
padding: 16rpx 24rpx;
border-radius: 8rpx;
padding: 20rpx 24rpx;
border-radius: 16rpx;
margin-bottom: 24rpx;
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: #FF9F43;
font-weight: 500;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
margin-bottom: 32rpx;
position: relative;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
font-size: 36rpx;
font-weight: 700;
color: #1F2937;
}
.close-icon {
position: absolute;
right: 30rpx;
font-size: 40rpx;
color: #999;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: 48rpx;
color: #9CA3AF;
line-height: 1;
padding: 10rpx;
transition: color 0.2s ease;
}
.close-icon:active {
color: #6B7280;
}
.popup-body {
padding: 30rpx;
padding: 16rpx 0 24rpx;
}
.amount-section {
text-align: center;
margin-bottom: 40rpx;
padding: 24rpx;
background: linear-gradient(145deg, #FFFFFF, #FFF8F3);
border-radius: 20rpx;
border: 1rpx solid rgba(255, 159, 67, 0.1);
}
.amount-section .label {
font-size: 28rpx;
color: #666;
color: #6B7280;
margin-right: 10rpx;
}
.amount-section .amount {
font-size: 48rpx;
font-weight: bold;
color: #ff4d4f;
font-size: 56rpx;
font-weight: 800;
background: linear-gradient(135deg, #FF6B35, #FF9F43);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-item {
margin-bottom: 30rpx;
margin-bottom: 24rpx;
}
.form-item .label {
display: block;
font-size: 28rpx;
color: #333;
color: #1F2937;
font-weight: 600;
margin-bottom: 16rpx;
}
.picker-display {
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
background: #f9f9f9;
background: #F9FAFB;
transition: all 0.2s ease;
}
.picker-display:active {
border-color: #FF9F43;
background: #FFF8F3;
}
.selected-text {
color: #333;
color: #1F2937;
font-weight: 500;
}
.placeholder {
color: #999;
color: #9CA3AF;
}
.arrow {
color: #ccc;
color: #9CA3AF;
width: 16rpx;
height: 16rpx;
border-right: 2rpx solid #ccc;
border-bottom: 2rpx solid #ccc;
border-right: 3rpx solid #9CA3AF;
border-bottom: 3rpx solid #9CA3AF;
transform: rotate(-45deg);
margin-right: 8rpx;
}
.popup-footer {
display: flex;
border-top: 1rpx solid #eee;
gap: 20rpx;
margin-top: 16rpx;
}
.btn-cancel, .btn-confirm {
flex: 1;
border: none;
background: #fff;
border-radius: 0;
font-size: 30rpx;
border-radius: 24rpx;
font-size: 32rpx;
padding: 24rpx 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: #6B7280;
background: #F3F4F6;
}
.btn-cancel:active {
background: #E5E7EB;
}
.btn-confirm {
color: #007AFF;
font-weight: bold;
color: #FFFFFF;
background: linear-gradient(135deg, #FF9F43, #FF6B35);
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
}
.btn-confirm:active {
transform: scale(0.97);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.25);
}
</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>
@ -291,121 +291,163 @@ async function onPaymentConfirm(paymentData) {
</script>
<style scoped>
/* ============================================
奇盒潮玩 - 选号组件 (适配高级卡片布局)
============================================ */
/* 容器 - 去除背景,融入父级卡片 */
.choice-grid-container {
padding: 20rpx;
padding: 10rpx 0;
background: transparent;
}
/* 加载和空状态 */
.loading-state, .empty-state {
text-align: center;
padding: 40rpx;
color: #999;
padding: 60rpx 0;
color: #9CA3AF;
font-size: 26rpx;
}
/* 网格包装 */
.grid-wrapper {
padding-bottom: 160rpx; /* 留出底部操作栏空间 */
}
/* 号码网格 - 8列布局 */
.choices-grid {
display: grid;
grid-template-columns: repeat(5, 1fr); /* 一行5个 */
gap: 16rpx;
margin-bottom: 120rpx; /* 留出底部操作栏空间 */
grid-template-columns: repeat(8, 1fr);
gap: 10rpx;
padding: 0;
}
/* 单个号码格子 */
.choice-item {
aspect-ratio: 1;
background: #fff;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
background: #F9FAFB;
border: 1rpx solid #E5E7EB;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
transition: all 0.2s;
}
.choice-item:active {
transform: scale(0.9);
}
/* 号码文字 */
.choice-number {
font-size: 32rpx;
font-weight: bold;
color: #333;
font-size: 24rpx;
font-weight: 700;
color: #4B5563;
z-index: 1;
}
/* 状态文字 - 简化为小点或隐藏 */
.choice-status {
font-size: 20rpx;
margin-top: 4rpx;
color: #666;
display: none;
}
/* 状态样式 */
/* ============= 状态样式 ============= */
/* 可选状态 */
.is-available {
background: #fff;
background: #F9FAFB;
}
/* 已售状态 */
.is-sold {
background: #f5f5f5;
border-color: #eee;
opacity: 0.6;
background: #F3F4F6;
border-color: #F3F4F6;
opacity: 0.8;
}
.is-sold::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.05);
}
.is-sold .choice-number {
color: #ccc;
color: #D1D5DB;
text-decoration: line-through;
}
/* 选中状态 - 橙色高亮 */
.is-selected {
background: #e6f7ff;
border-color: #1890ff;
background: linear-gradient(135deg, #FF9F43, #FF6B35);
border-color: transparent;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.4);
}
.is-selected .choice-number {
color: #1890ff;
}
.is-selected .choice-status {
color: #1890ff;
color: #FFFFFF;
}
/* 底部操作栏 */
/* ============= 底部操作栏 ============= */
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
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(255, 255, 255, 0.5);
}
/* 选择信息行 */
.selection-info {
font-size: 28rpx;
color: #333;
font-size: 26rpx;
color: #4B5563;
display: flex;
align-items: center;
}
.highlight {
color: #ff4d4f;
font-weight: bold;
margin: 0 4rpx;
color: #FF6B35;
font-weight: 800;
font-size: 36rpx;
margin: 0 8rpx;
}
/* 按钮组 */
.action-buttons {
display: flex;
gap: 16rpx;
}
/* 通用按钮样式 */
.btn-common {
height: 72rpx;
line-height: 72rpx;
padding: 0 40rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 600;
margin: 0;
}
/* 购买按钮 */
.btn-buy {
background: #ff4d4f;
color: #fff;
border-radius: 40rpx;
padding: 0 60rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
margin: 0;
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.3);
}
/* 随机按钮 */
.btn-random {
background: #007AFF;
color: #fff;
border-radius: 40rpx;
padding: 0 60rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
margin: 0;
background: #F3F4F6 !important;
color: #4B5563 !important;
}
</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

@ -293,39 +293,38 @@ onLoad((opts) => {
</script>
<style scoped>
.page { height: 100vh; padding-bottom: 140rpx }
/* 奇盒潮玩 - 对对碰活动页面 */
.page { height: 100vh; padding-bottom: 140rpx; background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%); }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.banner-img { width: 100%; border-radius: 20rpx; box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1); }
.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 }
.title { font-size: 40rpx; font-weight: 800; background: linear-gradient(135deg, #FF6B35, #FF9F43); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; }
.meta { margin-top: 12rpx; font-size: 28rpx; color: #6B7280; text-align: center; }
.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 }
.primary { background: linear-gradient(135deg, #FF9F43, #FF6B35) !important; color: #fff !important; box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.35); }
.float-actions { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 24rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20rpx); box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.08); z-index: 9999 }
.float-btn { width: 100%; border-radius: 999rpx; background: linear-gradient(135deg, #FF9F43, #FF6B35) !important; color: #fff !important; font-weight: 600; box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35); }
.issues { background: #FFFFFF; border-radius: 20rpx; margin: 0 24rpx 24rpx; padding: 20rpx; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06); }
.issues-title { font-size: 32rpx; font-weight: 700; color: #1F2937; margin-bottom: 16rpx; padding-left: 16rpx; position: relative; }
.issues-title::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 6rpx; height: 28rpx; background: linear-gradient(180deg, #FF9F43, #FF6B35); border-radius: 999rpx; }
.issue-picker { height: 200rpx; background: linear-gradient(145deg, #FFF8F3, #FFF4E6); border-radius: 16rpx; margin-bottom: 64rpx }
.picker-item { height: 40rpx; line-height: 40rpx; text-align: center; font-size: 28rpx; color: #1F2937; }
.tabs { display: flex; padding: 0 12rpx; margin-bottom: 20rpx; gap: 12rpx; }
.tab { flex: 1; text-align: center; font-size: 28rpx; padding: 18rpx 0; border: 2rpx solid rgba(255, 159, 67, 0.3); color: #B45309; background: linear-gradient(145deg, #FFFFFF, #FFF8F3); border-radius: 16rpx; font-weight: 500; transition: all 0.2s ease; }
.tab.active { background: linear-gradient(135deg, #FF9F43, #FF6B35); border-color: #FF6B35; color: #fff; font-weight: 600; box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.3); }
.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 }
.reward-card { background: #FFFFFF; border-radius: 16rpx; overflow: hidden; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06); margin-bottom: 16rpx }
.el-reward-card { margin-bottom: 16rpx }
.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 }
.el-card-title { font-size: 28rpx; color: #1F2937; flex: 1; margin-right: 8rpx; word-break: break-all; font-weight: 600; }
.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 }
.card-image { width: 100%; height: auto; display: block; background: linear-gradient(145deg, #FFF8F3, #FFF4E6); position: relative; z-index: 1 }
.prob-corner { position: absolute; background: linear-gradient(135deg, #FF6B35, #FF9F43); color: #fff; font-size: 22rpx; font-weight: 600; padding: 8rpx 16rpx; border-radius: 999rpx; z-index: 2; box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.35); }
.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 }
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 16rpx }
.card-title { font-size: 28rpx; color: #1F2937; flex: 1; margin-right: 8rpx; word-break: break-all; font-weight: 600; }
.badge-boss { background: linear-gradient(135deg, #FF9F43, #FFD166); color: #6b4b1f; font-size: 22rpx; font-weight: 600; padding: 6rpx 14rpx; border-radius: 999rpx }
.badge-count { background: linear-gradient(135deg, #FFD166, #FFE8A3); color: #6b4b1f; font-size: 22rpx; font-weight: 600; padding: 6rpx 14rpx; border-radius: 999rpx }
.rewards-empty, .issues-empty { font-size: 26rpx; color: #9CA3AF; text-align: center; padding: 40rpx }
</style>

View File

@ -0,0 +1,233 @@
<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()
})
</script>
<style scoped>
.page {
min-height: 100vh;
background: #F8F8F8;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
padding: 24rpx;
}
.activity-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.activity-item {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
}
.thumb-box {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
height: 0;
background: #f0f0f0;
}
.thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.tag-hot {
position: absolute;
top: 12rpx; left: 12rpx;
background: #333;
color: #FFD700;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 800;
}
.info {
padding: 20rpx 16rpx;
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
.name {
font-size: 28rpx;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 20rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 76rpx;
line-height: 1.35;
}
.bottom-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-text {
font-size: 24rpx;
color: #FF4D4F;
font-weight: 700;
}
.btn-go {
background: #1A1A1A;
color: #FFD700;
font-size: 24rpx;
font-weight: 900;
padding: 8rpx 24rpx;
border-radius: 999rpx;
}
.loading-wrap {
display: flex; justify-content: center; padding: 100rpx;
}
.spinner {
width: 48rpx; height: 48rpx;
border: 4rpx solid #ddd; border-top-color: #FF9F43;
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;
}
.empty-img {
width: 240rpx;
margin-bottom: 24rpx;
opacity: 0.4;
}
.empty-text {
color: #999;
font-size: 28rpx;
}
</style>

View File

@ -0,0 +1,374 @@
<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 scoped>
.page-wrapper {
min-height: 100vh;
position: relative;
background: #2D1B4E; /* Dark Purple Theme */
color: #fff;
display: flex;
flex-direction: column;
}
.bg-fixed {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
opacity: 0.3;
z-index: 0;
}
.bg-mask {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(180deg, rgba(45,27,78,0.8), #2D1B4E);
z-index: 1;
}
.content-area {
position: relative;
z-index: 2;
flex: 1;
display: flex;
flex-direction: column;
padding: 30rpx;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 60rpx;
}
.main-title { font-size: 48rpx; font-weight: 900; font-style: italic; display: block; }
.sub-title { font-size: 24rpx; opacity: 0.8; margin-top: 8rpx; display: block; }
.rule-btn {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
padding: 8rpx 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
}
.tower-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.tower-level {
width: 100%;
background: linear-gradient(135deg, #6D28D9, #4C1D95);
padding: 40rpx;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.3);
margin-bottom: 40rpx;
border: 2rpx solid rgba(255,255,255,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.level-info { display: flex; flex-direction: column; }
.level-num { font-size: 24rpx; opacity: 0.8; margin-bottom: 8rpx; }
.level-name { font-size: 40rpx; font-weight: 700; }
.level-status { font-size: 24rpx; background: rgba(0,0,0,0.2); padding: 4rpx 12rpx; border-radius: 8rpx; }
.rewards-preview {
width: 100%;
}
.rewards-scroll {
white-space: nowrap;
}
.reward-item {
display: inline-block;
width: 160rpx;
margin-right: 20rpx;
text-align: center;
}
.reward-img {
width: 120rpx; height: 120rpx;
border-radius: 16rpx;
background: rgba(255,255,255,0.1);
margin-bottom: 12rpx;
}
.reward-name { font-size: 22rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.reward-prob { font-size: 20rpx; color: #FFD700; }
.action-area {
background: rgba(0,0,0,0.4);
backdrop-filter: blur(20rpx);
padding: 30rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.price-display { color: #FFD700; font-weight: 700; }
.currency { font-size: 28rpx; }
.amount { font-size: 48rpx; margin: 0 4rpx; }
.unit { font-size: 24rpx; opacity: 0.8; }
.challenge-btn {
background: linear-gradient(135deg, #FFD700, #F59E0B);
color: #333;
font-weight: 900;
border-radius: 999rpx;
padding: 0 60rpx;
font-size: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(245, 158, 11, 0.4);
}
.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.8);
}
.flip-content {
position: relative; z-index: 2; height: 100%; display: flex; flex-direction: column; padding: 40rpx;
}
.close-btn {
margin-top: auto;
background: #fff; color: #333; border-radius: 999rpx; font-weight: 700;
}
</style>

View File

@ -1,25 +1,62 @@
<template>
<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>
@ -388,42 +425,48 @@ 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 }
/* 奇盒潮玩 - 无限赏活动页面 */
.page { height: 100vh; padding-bottom: 200rpx; background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%); }
.banner { padding: 24rpx 24rpx 0; }
.banner-img { width: 100%; border-radius: 20rpx; box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1); }
/* 商品信息卡片 */
.product-card { margin: 20rpx 24rpx; background: #FFFFFF; border-radius: 20rpx; padding: 20rpx; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06); }
.product-info { display: flex; align-items: flex-start; gap: 16rpx; }
.product-thumb { width: 120rpx; height: 120rpx; border-radius: 16rpx; flex-shrink: 0; background: linear-gradient(145deg, #FFF8F3, #FFF4E6); }
.product-detail { flex: 1; min-width: 0; }
.product-name { font-size: 30rpx; font-weight: 700; color: #1F2937; margin-bottom: 8rpx; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.product-price { font-size: 34rpx; font-weight: 800; color: #FF6B35; }
.product-actions { display: flex; flex-direction: column; gap: 12rpx; }
.action-btn { background: linear-gradient(145deg, #FFFFFF, #FFF8F3); border: 2rpx solid rgba(255, 159, 67, 0.3); border-radius: 12rpx; padding: 10rpx 14rpx; font-size: 22rpx; color: #B45309; text-align: center; }
.action-btn:active { background: #FFF4E6; }
/* 期号切换条 */
.issue-bar { display: flex; align-items: center; justify-content: center; gap: 20rpx; margin: 0 24rpx 20rpx; padding: 16rpx 20rpx; background: #FFFFFF; border-radius: 999rpx; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); }
.nav-btn { width: 64rpx; height: 64rpx; border-radius: 50%; background: linear-gradient(135deg, #FF9F43, #FF6B35); color: #FFFFFF; display: flex; align-items: center; justify-content: center; font-size: 28rpx; padding: 0; margin: 0; line-height: 1; box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3); }
.nav-btn:active { transform: scale(0.92); }
.issue-info { display: flex; flex-direction: column; align-items: center; gap: 4rpx; }
.issue-label { font-size: 28rpx; font-weight: 700; color: #1F2937; }
/* 玩法福利标签 */
.gameplay-tags { display: flex; gap: 12rpx; padding: 0 24rpx; margin-bottom: 20rpx; flex-wrap: wrap; }
.tag { padding: 10rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 600; }
.tag-pool { background: linear-gradient(135deg, #10B981, #34D399); color: #FFFFFF; box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.3); }
.tag-drop { background: linear-gradient(135deg, #FF9F43, #FF6B35); color: #FFFFFF; box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3); }
.tag-free { background: linear-gradient(135deg, #FFD166, #FF9F43); color: #6b4b1f; box-shadow: 0 4rpx 12rpx rgba(255, 159, 67, 0.3); }
/* 底部多档位抽赏按钮 */
.bottom-actions { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 12rpx; padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: linear-gradient(180deg, rgba(255,255,255,0.95) 0%, #FFFFFF 100%); backdrop-filter: blur(20rpx); box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.08); z-index: 999; }
.tier-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 12rpx 8rpx; background: linear-gradient(135deg, #FFD166, #FF9F43); border-radius: 16rpx; box-shadow: 0 4rpx 12rpx rgba(255, 159, 67, 0.3); }
.tier-btn:active { transform: scale(0.95); }
.tier-price { font-size: 24rpx; font-weight: 700; color: #6b4b1f; }
.tier-label { font-size: 22rpx; color: #8a5a2b; margin-top: 4rpx; }
.tier-hot { background: linear-gradient(135deg, #FF9F43, #FF6B35); }
.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 }
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1 }
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.6); 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 }
.overlay-close { background: linear-gradient(135deg, #FFD166, #FF9F43) !important; color: #6b4b1f !important; border-radius: 999rpx; align-self: flex-end; font-weight: 600; box-shadow: 0 6rpx 16rpx rgba(255, 159, 67, 0.35); }
</style>

View File

@ -1,32 +1,93 @@
<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="page-bg">
<image class="bg-image" :src="detail.banner" mode="aspectFill" />
<view class="bg-mask"></view>
</view>
</scroll-view>
<!-- 导航栏占位如果有自定义导航栏需求 -->
<!-- <view class="nav-bar-placeholder"></view> -->
<!-- 主要内容区域 -->
<scroll-view class="main-scroll" scroll-y>
<!-- 头部信息卡片 -->
<view class="header-card">
<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" 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">
<!-- 期号切换 -->
<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>
@ -65,6 +126,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 '进行中'
@ -299,54 +380,251 @@ 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;
/* ============================================
一番赏页面 - 高级设计重构
============================================ */
.page-wrapper {
min-height: 100vh;
background: #F2F3F7;
position: relative;
overflow: hidden;
}
/* 顶部背景 */
.page-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 600rpx;
z-index: 1;
}
.bg-image {
width: 100%;
height: 100%;
filter: blur(40rpx);
opacity: 0.8;
}
.bg-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(242,243,247,0.3) 0%, #F2F3F7 100%);
}
.main-scroll {
position: relative;
z-index: 2;
height: 100vh;
}
/* 头部卡片 */
.header-card {
margin: 30rpx 24rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 24rpx;
display: flex;
align-items: center;
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.6);
}
.header-cover {
width: 160rpx;
height: 160rpx;
border-radius: 20rpx;
margin-right: 24rpx;
background: #EEE;
box-shadow: 0 8rpx 16rpx rgba(0,0,0,0.1);
}
.header-info {
flex: 1;
}
.header-title {
font-size: 34rpx;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 12rpx;
line-height: 1.3;
}
.header-price-row {
display: flex;
align-items: baseline;
color: #FF6B35;
margin-bottom: 12rpx;
}
.price-symbol { font-size: 24rpx; font-weight: 600; }
.price-num { font-size: 40rpx; font-weight: 800; margin: 0 4rpx; }
.price-unit { font-size: 24rpx; color: #999; }
.header-tags {
display: flex;
gap: 12rpx;
}
.tag-item {
font-size: 20rpx;
color: #B45309;
background: #FFF4E6;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.header-actions {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-left: 20rpx;
padding-left: 20rpx;
border-left: 1rpx solid #EEE;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: 20rpx;
color: #666;
}
.action-btn .icon {
font-size: 32rpx;
margin-bottom: 4rpx;
}
/* 通用板块容器 */
.section-container {
margin: 24rpx;
background: #FFFFFF;
border-radius: 32rpx;
padding: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
}
/* 板块标题 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #1A1A1A;
}
.section-more {
font-size: 24rpx;
color: #999;
}
/* 奖品概览 */
.preview-scroll {
white-space: nowrap;
}
.preview-item {
display: inline-block;
width: 180rpx;
margin-right: 20rpx;
vertical-align: top;
}
.preview-img {
width: 180rpx;
height: 180rpx;
border-radius: 20rpx;
background: #F8F8F8;
margin-bottom: 12rpx;
box-shadow: inset 0 0 0 1rpx rgba(0,0,0,0.03);
}
.preview-name {
font-size: 24rpx;
color: #333;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.prize-tag {
position: absolute;
top: 8rpx;
left: 8rpx;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 18rpx;
padding: 2rpx 8rpx;
border-radius: 6rpx;
z-index: 10;
}
.prize-tag.tag-boss {
background: linear-gradient(135deg, #FF9F43, #FF6B35);
}
/* 选号区容器 */
.selector-container {
min-height: 600rpx;
display: flex;
flex-direction: column;
}
/* 期号头部 */
.issue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
background: #F9FAFB;
border-radius: 20rpx;
padding: 12rpx;
}
.issue-switch-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
background: #FFFFFF;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
}
.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 }
.issue-switch-btn:active {
transform: scale(0.95);
background: #F0F0F0;
}
.arrow {
font-size: 24rpx;
color: #999;
}
.issue-info-center {
display: flex;
flex-direction: column;
align-items: center;
}
.issue-current-text {
font-size: 30rpx;
font-weight: 700;
color: #333;
}
.issue-status-badge {
font-size: 20rpx;
color: #10B981;
background: #D1FAE5;
padding: 2rpx 12rpx;
border-radius: 999rpx;
margin-top: 4rpx;
}
.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.6); z-index: 1; }
.flip-content { position: relative; display: flex; flex-direction: column; height: 100%; padding: 24rpx; z-index: 2; }
.overlay-close { background: linear-gradient(135deg, #FFD93D, #FFB800) !important; color: #6b4b1f !important; border-radius: 999rpx; align-self: flex-end; font-weight: 600; box-shadow: 0 6rpx 16rpx rgba(255, 184, 0, 0.35); }
</style>

View File

@ -149,10 +149,75 @@ 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 }
/* ============================================
奇盒潮玩 - 地址编辑页面
采用暖橙色调的表单设计
============================================ */
.wrap {
padding: 24rpx;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%);
}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.label {
width: 160rpx;
font-size: 28rpx;
font-weight: 500;
color: #6B7280;
flex-shrink: 0;
}
.input {
flex: 1;
font-size: 28rpx;
color: #1F2937;
background: transparent;
}
/* 提交按钮 */
.submit {
width: 100%;
height: 88rpx;
line-height: 88rpx;
margin-top: 32rpx;
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
transition: all 0.2s ease;
}
.submit:active {
transform: scale(0.97);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.25);
}
.submit[disabled] {
opacity: 0.6;
box-shadow: none;
}
/* 错误提示 */
.error {
color: #EF4444;
font-size: 26rpx;
margin-top: 16rpx;
padding: 16rpx;
background: rgba(239, 68, 68, 0.1);
border-radius: 12rpx;
text-align: center;
}
</style>

View File

@ -114,17 +114,131 @@ 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 }
/* ============================================
奇盒潮玩 - 地址管理页面
采用暖橙色调的卡片列表设计
============================================ */
.wrap {
padding: 24rpx;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%);
}
.header {
display: flex;
justify-content: flex-end;
margin-bottom: 20rpx;
}
.add {
font-size: 28rpx;
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
border-radius: 999rpx;
padding: 0 32rpx;
height: 72rpx;
line-height: 72rpx;
font-weight: 600;
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.35);
}
.add:active {
transform: scale(0.96);
}
/* 地址卡片 */
.addr {
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.addr-main {
margin-bottom: 16rpx;
}
.addr-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.addr-row:last-child {
margin-bottom: 0;
}
.name {
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
}
.phone {
font-size: 28rpx;
color: #6B7280;
}
.default {
font-size: 22rpx;
color: #FFFFFF;
background: linear-gradient(135deg, #FF9F43, #FF6B35);
padding: 4rpx 12rpx;
border-radius: 999rpx;
font-weight: 500;
}
.region {
font-size: 26rpx;
color: #6B7280;
}
.detail {
font-size: 26rpx;
color: #1F2937;
line-height: 1.5;
}
/* 操作按钮 */
.addr-actions {
display: flex;
justify-content: flex-end;
gap: 12rpx;
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #F3F4F6;
}
.addr-actions button {
font-size: 26rpx;
height: 56rpx;
line-height: 56rpx;
padding: 0 24rpx;
border-radius: 28rpx;
margin: 0;
}
.addr-actions button[type="warn"] {
background: #FEE2E2 !important;
color: #EF4444 !important;
}
.addr-actions button:not([type]) {
background: #F3F4F6 !important;
color: #6B7280 !important;
}
.addr-actions button:not([type]):active {
background: #E5E7EB !important;
}
/* 空状态 */
.empty {
text-align: center;
color: #9CA3AF;
margin-top: 120rpx;
font-size: 28rpx;
}
/* 错误提示 */
.error {
color: #EF4444;
font-size: 26rpx;
margin-bottom: 16rpx;
padding: 16rpx;
background: rgba(239, 68, 68, 0.1);
border-radius: 12rpx;
text-align: center;
}
</style>

View File

@ -570,49 +570,47 @@ async function onShip() {
</script>
<style scoped>
/* ============================================
奇盒潮玩 - 货柜页面
采用暖橙色调物品卡片式布局
============================================ */
.item-status {
font-size: 24rpx;
color: #007AFF;
color: #FF9F43;
margin-top: 4rpx;
font-weight: 500;
}
.item-meta {
font-size: 22rpx;
color: #9CA3AF;
margin-top: 4rpx;
}
.item-meta { font-size: 22rpx; color: #666; margin-top: 4rpx }
.wrap { padding: 30rpx; }
.wrap {
padding: 24rpx;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%);
}
/* Tab 切换 */
.tabs {
display: flex;
background: #f5f5f5;
border-radius: 16rpx;
background: #FFFFFF;
border-radius: 20rpx;
padding: 8rpx;
margin-bottom: 20rpx;
}
.action-bar {
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;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.tab-item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
color: #6B7280;
padding: 20rpx 0;
border-radius: 12rpx;
transition: all 0.3s ease;
border-radius: 16rpx;
transition: all 0.25s ease;
display: flex;
justify-content: center;
align-items: center;
@ -620,10 +618,10 @@ async function onShip() {
}
.tab-item.active {
background: #fff;
color: #007AFF;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
background: linear-gradient(135deg, #FF9F43, #FF6B35);
color: #FFFFFF;
font-weight: 600;
box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.35);
}
.tab-text {
@ -632,36 +630,72 @@ async function onShip() {
.tab-count {
font-size: 24rpx;
opacity: 0.8;
opacity: 0.85;
}
.header { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
.status-text { text-align: center; color: #999; margin-top: 100rpx; }
/* 操作栏 */
.action-bar {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.select-all {
display: flex;
align-items: center;
font-size: 28rpx;
color: #1F2937;
font-weight: 500;
}
.select-all .checkbox {
margin-right: 12rpx;
}
.header {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 24rpx;
}
.status-text {
text-align: center;
color: #9CA3AF;
margin-top: 120rpx;
font-size: 28rpx;
}
/* 物品列表 */
.inventory-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
gap: 16rpx;
}
.inventory-item {
background: #fff;
border-radius: 12rpx;
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
flex-direction: row;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
}
.inventory-item:active {
transform: scale(0.98);
}
.item-image {
width: 120rpx;
height: 120rpx;
width: 140rpx;
height: 140rpx;
margin-right: 24rpx;
margin-bottom: 0;
border-radius: 8rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
background: linear-gradient(145deg, #FFF8F3, #FFF4E6);
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.item-info {
@ -673,10 +707,11 @@ async function onShip() {
}
.item-name {
font-size: 26rpx;
color: #333;
font-size: 28rpx;
color: #1F2937;
font-weight: 600;
display: block;
margin-bottom: 4rpx;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -684,15 +719,17 @@ async function onShip() {
.item-count {
font-size: 24rpx;
color: #999;
color: #9CA3AF;
margin-bottom: 4rpx;
}
.item-price {
font-size: 24rpx;
color: #ff4d4f;
font-size: 26rpx;
color: #FF6B35;
font-weight: 600;
}
/* 复选框 */
.checkbox-area {
padding: 10rpx 20rpx 10rpx 0;
display: flex;
@ -700,16 +737,18 @@ async function onShip() {
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ccc;
width: 44rpx;
height: 44rpx;
border: 3rpx solid #E5E7EB;
border-radius: 50%;
position: relative;
transition: all 0.2s ease;
}
.checkbox.checked {
background-color: #007AFF;
border-color: #007AFF;
background: linear-gradient(135deg, #FF9F43, #FF6B35);
border-color: #FF9F43;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.35);
}
.checkbox.checked::after {
@ -718,14 +757,15 @@ async function onShip() {
top: 50%;
left: 50%;
transform: translate(-50%, -60%) rotate(45deg);
width: 10rpx;
height: 20rpx;
width: 12rpx;
height: 22rpx;
border-right: 4rpx solid #fff;
border-bottom: 4rpx solid #fff;
}
/* 数量步进器 */
.item-actions {
margin-top: 10rpx;
margin-top: 12rpx;
display: flex;
align-items: center;
}
@ -733,44 +773,53 @@ async function onShip() {
.stepper {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 8rpx;
height: 48rpx;
background: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 12rpx;
height: 52rpx;
overflow: hidden;
}
.step-btn {
width: 48rpx;
width: 52rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
background: transparent;
font-size: 32rpx;
color: #666;
color: #6B7280;
transition: all 0.15s ease;
}
.step-btn:active {
background: #FFF4E6;
color: #FF6B35;
}
.step-btn.minus { border-right: 1px solid #ddd; }
.step-btn.plus { border-left: 1px solid #ddd; }
.step-btn.minus { border-right: 2rpx solid #E5E7EB; }
.step-btn.plus { border-left: 2rpx solid #E5E7EB; }
.step-num {
width: 60rpx;
width: 64rpx;
text-align: center;
font-size: 26rpx;
color: #333;
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #fff;
height: 110rpx;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
padding: 0 24rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
z-index: 100;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
@ -778,37 +827,52 @@ async function onShip() {
.selected-info {
font-size: 28rpx;
color: #333;
font-weight: bold;
color: #1F2937;
font-weight: 600;
}
.btn-group {
display: flex;
gap: 20rpx;
gap: 16rpx;
}
.action-btn {
margin: 0;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
border-radius: 32rpx;
padding: 0 40rpx;
height: 72rpx;
line-height: 72rpx;
font-size: 28rpx;
font-weight: 600;
border-radius: 36rpx;
padding: 0 36rpx;
border: none;
transition: all 0.2s ease;
}
.action-btn:active {
transform: scale(0.95);
}
.btn-ship {
background-color: #f0ad4e;
color: #fff;
background: linear-gradient(135deg, #FFD166, #FF9F43);
color: #6b4b1f;
box-shadow: 0 6rpx 16rpx rgba(255, 159, 67, 0.35);
}
.btn-redeem {
background-color: #dd524d;
color: #fff;
background: linear-gradient(135deg, #FF9F43, #FF6B35);
color: #FFFFFF;
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.35);
}
.loading-more, .no-more {
text-align: center;
color: #9CA3AF;
padding: 24rpx 0;
font-size: 26rpx;
}
.bottom-spacer {
height: 120rpx;
height: calc(120rpx + constant(safe-area-inset-bottom));
height: calc(120rpx + env(safe-area-inset-bottom));
height: 130rpx;
height: calc(130rpx + constant(safe-area-inset-bottom));
height: calc(130rpx + env(safe-area-inset-bottom));
}
</style>

View File

@ -1,44 +1,125 @@
<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="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 +161,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 +188,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 +217,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 +225,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 +238,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 +249,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 +288,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 +300,386 @@ 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: '知道了'
})
}
}
}
</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 }
/* ============================================
奇盒潮玩 - 首页样式 (V6.0 新版)
============================================ */
.page {
padding: 0;
background: #FFFFFF;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ========== 顶部导航栏 ========== */
.nav-header {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background: #FFF8F3;
padding-top: calc(20rpx + env(safe-area-inset-top));
gap: 24rpx;
}
.brand-logo {
display: flex;
align-items: center;
}
.brand-text {
font-size: 36rpx;
font-weight: 900;
color: #1A1A1A;
font-style: italic;
letter-spacing: -1rpx;
}
.brand-star {
font-size: 24rpx;
margin-left: 4rpx;
margin-top: -12rpx;
}
.search-bar {
flex: 1;
height: 64rpx;
background: white;
border-radius: 999rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
border: 1rpx solid #F0F0F0;
}
.search-icon { margin-right: 12rpx; font-size: 28rpx; }
.search-placeholder { color: #999; font-size: 28rpx; }
/* ========== 滚动主内容区 ========== */
.main-content {
flex: 1;
background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 600rpx);
padding-top: 20rpx;
}
/* Logo Banner */
.banner-box {
margin: 0 24rpx 24rpx;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06);
}
.banner-swiper, .banner-image, .banner-fallback {
width: 100%;
height: 320rpx;
}
.banner-fallback {
background: #FFFBE8;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2rpx dashed #FFE58F;
}
.banner-fallback-text { font-size: 48rpx; font-weight: 900; color: #1A1A1A; font-style: italic; margin-bottom: 12rpx; }
.banner-tag { background: #CCFF00; padding: 4rpx 16rpx; border-radius: 999rpx; font-size: 24rpx; font-weight: 700; color: #1A1A1A; }
/* 通知栏 */
.notice-bar {
margin: 0 24rpx 32rpx;
background: #F9F9F9;
border-radius: 12rpx;
padding: 16rpx 20rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.notice-tag {
background: #333;
color: #fff;
font-size: 22rpx;
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-weight: 700;
}
.notice-swiper { flex: 1; height: 36rpx; }
.notice-item { font-size: 24rpx; color: #333; line-height: 36rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.notice-more { font-size: 24rpx; color: #999; }
/* 玩法专区 - 方案B2+3 杂志风布局 */
.gameplay-section {
padding: 0 24rpx;
margin-bottom: 24rpx;
}
.section-header {
margin-bottom: 16rpx;
display: flex;
align-items: center;
}
.section-title {
font-size: 34rpx;
font-weight: 900;
color: #1A1A1A;
position: relative;
z-index: 1;
padding-bottom: 4rpx;
font-style: italic;
letter-spacing: 2rpx;
}
/* 黄色高光底线效果 */
.section-title::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 16rpx;
background: #FFD700;
z-index: -1;
border-radius: 4rpx;
transform: skewX(-10deg);
}
.gameplay-grid-v2 {
display: flex;
flex-direction: column;
gap: 16rpx;
}
/* 上排 */
.grid-row-top {
display: flex;
gap: 16rpx;
height: 180rpx; /* 大卡片高度 */
}
.game-card-large {
flex: 1;
border-radius: 20rpx;
position: relative;
overflow: hidden;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.05);
}
/* 下排 */
.grid-row-bottom {
display: flex;
gap: 16rpx;
height: 140rpx; /* 小卡片高度 */
}
.game-card-small {
flex: 1;
border-radius: 16rpx;
position: relative;
overflow: hidden;
padding: 16rpx;
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
background: white; /* 默认底色 */
}
/* 内容样式 - 大卡片 */
.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: 36rpx;
font-weight: 900;
color: #FFF;
font-style: italic;
margin-bottom: 12rpx;
text-shadow: 2rpx 2rpx 0 rgba(0,0,0,0.1);
}
.card-tag-large {
font-size: 20rpx;
background: #FFF;
color: #333;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 700;
box-shadow: 2rpx 2rpx 0 rgba(0,0,0,0.1);
transform: skewX(-10deg);
}
.card-tag-large.yellow { color: #875700; }
.card-mascot-large {
position: absolute;
right: -10rpx;
bottom: -20rpx;
width: 160rpx;
height: 160rpx;
transform: rotate(5deg);
}
/* 内容样式 - 小卡片 */
.card-title-small {
font-size: 28rpx;
font-weight: 800;
color: #333;
margin-bottom: 4rpx;
z-index: 2;
}
.card-subtitle-small {
font-size: 20rpx;
color: #888;
z-index: 2;
}
.card-icon-small {
position: absolute;
right: 0;
bottom: 0;
width: 90rpx;
height: 90rpx;
opacity: 0.8;
}
/* 背景配色 - 仿参考图Pop风格 */
.card-yifan {
background: linear-gradient(135deg, #FF9C6E 0%, #FF7875 100%); /* 粉红/橘色 */
}
.card-wuxian {
background: linear-gradient(135deg, #FFD666 0%, #FFA940 100%); /* 黄色/橙色 */
}
.card-match {
background: linear-gradient(135deg, #FFADD2 0%, #FF85C0 100%); /* 粉色 */
}
.card-tower {
background: linear-gradient(135deg, #B37FEB 0%, #9254DE 100%); /* 紫色 */
}
.card-more {
background: linear-gradient(135deg, #E0E0E0 0%, #F5F5F5 100%); /* 灰色 */
}
/* 对对碰文字颜色适配 */
.card-match .card-title-small { color: #FFF; }
.card-match .card-subtitle-small { color: rgba(255,255,255,0.8); }
.card-tower .card-title-small { color: #FFF; }
.card-tower .card-subtitle-small { color: rgba(255,255,255,0.8); }
/* 推荐活动列表 */
.activity-section {
padding: 0 24rpx;
}
.activity-grid-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.activity-item {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
}
.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;
}
.banner-fallback.mini {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background: #F5F5F5;
display: flex;
align-items: center;
justify-content: center;
}
.activity-tag-hot {
position: absolute;
top: 12rpx;
left: 12rpx;
background: rgba(0,0,0,0.6);
color: #FFD700;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 700;
backdrop-filter: blur(4rpx);
}
.activity-info {
padding: 20rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.activity-name {
font-size: 28rpx;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 16rpx;
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: 24rpx;
color: #FF4D4F; /* 价格/品类突出颜色 */
font-weight: 600;
max-width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.activity-btn-go {
background: #1A1A1A;
color: #FFD700;
font-size: 20rpx;
font-weight: 900;
padding: 6rpx 20rpx;
border-radius: 30rpx;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.15);
}
/* 空状态 */
.activity-empty {
text-align: center;
padding: 60rpx 0;
color: #9CA3AF;
font-size: 28rpx;
}
</style>

View File

@ -1,74 +1,103 @@
<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">
<!-- 品牌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 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 +112,95 @@ 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)
// ...
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
}
@ -205,62 +209,175 @@ 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 }
/* Page Container */
.container {
min-height: 100vh;
position: relative;
background: #F8F5F2; /* Cream base */
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Orbs Background */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(60rpx);
z-index: 0;
}
.orb-1 {
width: 400rpx;
height: 400rpx;
background: rgba(255, 107, 0, 0.15);
top: -100rpx;
left: -100rpx;
}
.orb-2 {
width: 500rpx;
height: 500rpx;
background: rgba(255, 215, 0, 0.1);
bottom: -150rpx;
right: -150rpx;
}
.content-wrap {
position: relative;
z-index: 1;
padding: 0 60rpx;
width: 100%;
box-sizing: border-box;
}
/* Brand Section */
.brand-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 80rpx;
}
.logo-box {
width: 180rpx;
height: 180rpx;
background: #FFFFFF;
border-radius: 40rpx;
padding: 20rpx;
box-shadow: 0 20rpx 60rpx rgba(255, 107, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-20rpx); }
100% { transform: translateY(0); }
}
.logo { width: 100%; height: 100%; }
.app-name { font-size: 48rpx; font-weight: 900; color: #1A1A1A; margin-bottom: 8rpx; letter-spacing: 2rpx; }
.welcome-text { font-size: 28rpx; color: #888; letter-spacing: 4rpx; }
/* Form Styles */
.input-group {
background: #FFFFFF;
border-radius: 999rpx;
height: 100rpx;
display: flex;
align-items: center;
padding: 0 32rpx;
margin-bottom: 32rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
}
.input-group:focus-within {
border-color: #FF6B00;
box-shadow: 0 0 0 6rpx rgba(255, 107, 0, 0.1);
transform: translateY(-2rpx);
}
.input-icon { width: 40rpx; height: 40rpx; margin-right: 20rpx; opacity: 0.6; }
.input-icon image { width: 100%; height: 100%; }
.input-field { flex: 1; height: 100%; font-size: 30rpx; color: #333; }
.input-placeholder { color: #BBB; }
.options-row { display: flex; justify-content: space-between; margin-bottom: 60rpx; }
.remember-box { display: flex; align-items: center; }
.checkbox { width: 36rpx; height: 36rpx; border: 3rpx solid #DDD; border-radius: 8rpx; margin-right: 12rpx; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
.checkbox.checked { background: #FF6B00; border-color: #FF6B00; }
.check-mark { color: #FFF; font-size: 24rpx; font-weight: bold; }
.remember-text { font-size: 26rpx; color: #666; }
/* Buttons */
.btn {
height: 100rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 800;
position: relative;
overflow: hidden;
transition: all 0.2s;
}
.btn:active { transform: scale(0.96); }
.login-btn {
background: linear-gradient(90deg, #FF6B00 0%, #FFA500 100%);
color: #FFF;
box-shadow: 0 10rpx 30rpx rgba(255, 107, 0, 0.3);
margin-bottom: 32rpx;
}
.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;
color: #FFF;
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
}
.wx-icon { width: 48rpx; height: 48rpx; margin-right: 16rpx; }
/* Register Link */
.register-link { text-align: center; margin-top: 32rpx; }
.register-text { font-size: 28rpx; color: #888; }
.highlight { color: #FF6B00; font-weight: 700; margin-left: 8rpx; }
/* Agreements */
.agreements {
margin-top: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.checkbox.round { border-radius: 50%; width: 32rpx; height: 32rpx; }
.checkbox-area { padding: 10rpx; }
.agreement-text { font-size: 24rpx; color: #999; margin-left: 8rpx; }
.link { color: #FF6B00; text-decoration: underline; margin: 0 4rpx; }
.error-toast {
position: fixed;
top: 100rpx;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 60, 60, 0.9);
color: #fff;
padding: 16rpx 32rpx;
border-radius: 12rpx;
font-size: 26rpx;
z-index: 999;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -164,19 +164,127 @@ 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 }
/* ============================================
奇盒潮玩 - 订单页面
采用暖橙色调的订单列表设计
============================================ */
.wrap {
padding: 24rpx;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%);
}
/* Tab 切换 */
.tabs {
display: flex;
background: #FFFFFF;
border-radius: 20rpx;
padding: 8rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #6B7280;
border-radius: 16rpx;
transition: all 0.25s ease;
font-weight: 500;
}
.tab.active {
background: linear-gradient(135deg, #FF9F43, #FF6B35);
color: #FFFFFF;
font-weight: 600;
box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.35);
}
/* 订单卡片 */
.order {
display: flex;
justify-content: space-between;
align-items: center;
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
}
.order:active {
transform: scale(0.98);
}
.order-main {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.order-title {
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.order-sub {
font-size: 24rpx;
color: #9CA3AF;
margin-top: 8rpx;
}
.order-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 16rpx;
flex-shrink: 0;
}
.order-amount {
font-size: 30rpx;
font-weight: 700;
background: linear-gradient(135deg, #FF6B35, #FF9F43);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.order-status {
font-size: 24rpx;
color: #6B7280;
margin-top: 8rpx;
padding: 4rpx 12rpx;
background: #F3F4F6;
border-radius: 999rpx;
}
/* 空状态 */
.empty {
text-align: center;
color: #9CA3AF;
margin-top: 120rpx;
font-size: 28rpx;
}
/* 错误提示 */
.error {
color: #EF4444;
font-size: 26rpx;
margin-bottom: 16rpx;
padding: 16rpx;
background: rgba(239, 68, 68, 0.1);
border-radius: 12rpx;
text-align: center;
}
/* 加载状态 */
.loading, .end {
text-align: center;
color: #9CA3AF;
padding: 24rpx 0;
font-size: 26rpx;
}
</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>

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

@ -0,0 +1,127 @@
<template>
<view class="page">
<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 scoped>
/* ============================================
奇盒潮玩 - 商品详情页
============================================ */
.page {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F3 0%, #FFFFFF 100%);
}
.loading, .empty {
text-align: center;
padding: 120rpx 40rpx;
color: #9CA3AF;
font-size: 28rpx;
}
.detail-wrap {
padding-bottom: 40rpx;
}
.main-image {
width: 100%;
display: block;
}
.info-card {
margin: 24rpx;
background: #FFFFFF;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.title {
font-size: 34rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 16rpx;
}
.price-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 12rpx;
}
.price {
font-size: 40rpx;
font-weight: 800;
background: linear-gradient(135deg, #FF6B35, #FF9F43);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.points {
font-size: 26rpx;
color: #FF9F43;
padding: 4rpx 12rpx;
background: rgba(255, 159, 67, 0.15);
border-radius: 999rpx;
}
.stock {
font-size: 26rpx;
color: #6B7280;
margin-bottom: 16rpx;
}
.desc {
font-size: 28rpx;
color: #4B5563;
line-height: 1.7;
}
</style>

View File

@ -2,51 +2,74 @@
<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-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 class="check-box" :class="{ on: hideForever }">
<text v-if="hideForever" class="iconfont icon-check"></text>
</view>
<text class="check-text">不再提示</text>
</view>
<button class="notice-btn" type="primary" @tap.stop="onDismissNotice">我知道了</button>
<button class="notice-btn" hover-class="btn-hover" @tap.stop="onDismissNotice">我知道了</button>
</view>
</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 class="header-section">
<view class="search-box" style="margin-top: 20rpx;">
<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 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="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 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 +79,32 @@ 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 +126,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 +151,6 @@ function applyFilters() {
}
return true
})
distributeToColumns(filtered)
}
function onSearchConfirm() { applyFilters() }
@ -198,56 +158,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,21 +218,16 @@ onShow(async () => {
})
return
}
// Notice logic
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 (_) {}
} catch (_) { showNotice.value = true }
loading.value = true
await loadProducts()
loading.value = false
@ -297,44 +246,318 @@ function onDismissNotice() {
</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 }
.page {
min-height: 100vh;
background-color: #F5F6F8;
padding-bottom: 40rpx;
}
/* 顶部 Header */
.header-section {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #FFFFFF;
padding: 0 24rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
}
.header-placeholder {
height: 160rpx; /* 根据 header 高度调整 */
}
.page-title {
font-size: 36rpx;
font-weight: 800;
color: #111;
padding: 20rpx 0;
}
/* 搜索框 */
.search-box {
margin-bottom: 20rpx;
}
.search-input-wrap {
display: flex;
align-items: center;
background: #F5F7FA;
border-radius: 16rpx;
padding: 18rpx 24rpx;
transition: all 0.3s;
}
.search-input-wrap:focus-within {
background: #FFF;
box-shadow: 0 0 0 2rpx #FF9F43;
}
.search-icon {
font-size: 28rpx;
margin-right: 16rpx;
opacity: 0.5;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.placeholder-style {
color: #999;
}
/* 筛选行 */
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.price-range {
flex: 1;
display: flex;
align-items: center;
background: #F5F7FA;
border-radius: 12rpx;
padding: 10rpx 20rpx;
}
.price-label {
font-size: 24rpx;
color: #666;
margin-right: 16rpx;
}
.price-input {
flex: 1;
font-size: 26rpx;
text-align: center;
color: #333;
}
.price-ph {
color: #BBB;
font-size: 24rpx;
}
.price-sep {
color: #CCC;
margin: 0 10rpx;
}
.filter-btn {
background: linear-gradient(135deg, #FF9F43, #FF6B35);
color: white;
font-size: 26rpx;
font-weight: 600;
border-radius: 12rpx;
padding: 0 32rpx;
height: 64rpx;
line-height: 64rpx;
border: none;
}
.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-card {
background: #FFFFFF;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
}
.thumb-wrap {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 Aspect Ratio */
background: #F9F9F9;
}
.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(0,0,0,0.6);
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-top-left-radius: 12rpx;
}
.product-info {
padding: 20rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-title {
font-size: 28rpx;
color: #333;
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;
}
.product-bottom {
display: flex;
align-items: flex-end;
justify-content: space-between;
flex-wrap: wrap;
gap: 8rpx;
}
.price-row {
color: #FF5500;
font-weight: 700;
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 24rpx;
}
.price-val {
font-size: 34rpx;
}
.points-badge {
background: #FFF0E6;
color: #FF6B35;
border: 1px solid rgba(255, 107, 53, 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 #ddd;
border-top-color: #FF9F43;
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: #999;
font-size: 28rpx;
}
/* 弹窗样式 */
.notice-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.notice-dialog {
width: 560rpx;
background: #FFF;
border-radius: 24rpx;
padding: 40rpx 32rpx;
text-align: center;
}
.notice-title {
font-size: 34rpx;
font-weight: 700;
margin-bottom: 24rpx;
color: #333;
}
.notice-content {
font-size: 28rpx;
color: #555;
line-height: 1.6;
margin-bottom: 40rpx;
text-align: left;
}
.notice-actions {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.notice-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(90deg, #FF9F43, #FF6B35);
color: #fff;
border-radius: 40rpx;
font-size: 30rpx;
font-weight: 600;
}
.notice-check {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
opacity: 0.8;
}
.check-box {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #CCC;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.check-box.on {
background: #FF6B35;
border-color: #FF6B35;
}
.icon-check {
font-size: 20rpx;
color: #FFF;
}
.check-text {
font-size: 26rpx;
color: #888;
}
</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
}
}

201
uni.scss
View File

@ -1,76 +1,175 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
* 奇盒潮玩 - 全局样式系统
*
* 基于潮玩盲盒风格的设计系统采用暖橙渐变色调
* 传递惊喜期待活力的品牌调性
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
/* ============================================
🎨 品牌色彩系统 - 潮玩暖色调
============================================ */
/* 颜色变量 */
/* 主色 - 暖橙渐变 */
$primary-orange: #FF9F43; // 活力橙
$primary-deep: #FF6B35; // 深橙红
$primary-light: #FFB366; // 浅橙
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 辅助色 */
$accent-gold: #FFD166; // 金币黄
$accent-pink: #FF8FAB; // 少女粉
$accent-coral: #FF7B7B; // 珊瑚红
$accent-purple: #A78BFA; // 梦幻紫
/* 功能色 */
$success-color: #10B981; // 成功绿
$warning-color: #FBBF24; // 警告黄
$error-color: #EF4444; // 错误红
$info-color: #3B82F6; // 信息蓝
/* 中性色 */
$text-primary: #1F2937; // 主要文字
$text-secondary: #6B7280; // 次要文字
$text-tertiary: #9CA3AF; // 辅助文字
$text-inverse: #FFFFFF; // 反色文字
/* 背景色 */
$bg-page: #FFF8F3; // 页面暖白底
$bg-card: #FFFFFF; // 卡片白
$bg-warm: #FFF4E6; // 暖色面板
$bg-grey: #F9FAFB; // 冷灰面板
/* 边框色 */
$border-light: #F3F4F6;
$border-normal: #E5E7EB;
$border-warm: rgba(255, 159, 67, 0.2);
/* ============================================
渐变预设
============================================ */
/* 注意Sass变量不能存储CSS渐变值用于直接引用
以下为文档记录使用时直接写CSS */
// 主渐变linear-gradient(135deg, #FF9F43, #FF6B35)
// 金色渐变linear-gradient(135deg, #FFD166, #FF9F43)
// 粉色渐变linear-gradient(135deg, #FF8FAB, #FF6B81)
// 卡片高光linear-gradient(145deg, #FFFFFF, #FFF8F3)
/* ============================================
📐 间距与圆角
============================================ */
/* 间距 */
$spacing-xs: 8rpx;
$spacing-sm: 12rpx;
$spacing-md: 16rpx;
$spacing-lg: 24rpx;
$spacing-xl: 32rpx;
$spacing-2xl: 48rpx;
/* 圆角 */
$radius-sm: 8rpx;
$radius-md: 12rpx;
$radius-lg: 16rpx;
$radius-xl: 24rpx;
$radius-full: 999rpx;
/* ============================================
🔤 字体系统
============================================ */
$font-size-xs: 22rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-md: 30rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
$font-size-2xl: 44rpx;
$font-size-3xl: 56rpx;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
/* ============================================
🌟 阴影效果
============================================ */
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
$shadow-md: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
$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: $primary-orange;
$uni-color-success: $success-color;
$uni-color-warning: $warning-color;
$uni-color-error: $error-color;
/* 文字基本颜色 */
$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-primary;
$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-grey;
$uni-bg-color-hover: #FFF0E6;
$uni-bg-color-mask: rgba(0, 0, 0, 0.5);
/* 边框颜色 */
$uni-border-color:#c8c7cc;
$uni-border-color: $border-normal;
/* 尺寸变量 */
$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-primary;
$uni-font-size-title: 20px;
$uni-color-subtitle: $text-secondary;
$uni-font-size-subtitle: 16px;
$uni-color-paragraph: $text-secondary;
$uni-font-size-paragraph: 15px;