Compare commits
4 Commits
dcb3f8cc41
...
6f7207da2d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f7207da2d | |||
| dd12d8e500 | |||
| f8b1f57cd7 | |||
| 8a6ac48d59 |
45
App.vue
45
App.vue
@ -17,6 +17,49 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
/*每个页面公共css */
|
||||
@import '@/uni.scss';
|
||||
|
||||
page {
|
||||
background-color: $bg-page;
|
||||
color: $text-main;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
/* Global Keyframes */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes fadeInDown {
|
||||
from { opacity: 0; transform: translateY(-30rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(30rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(100%); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes zoomIn {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-20rpx); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -5,8 +5,8 @@ export function wechatLogin(code, invite_code) {
|
||||
return request({ url: '/api/app/users/weixin/login', method: 'POST', data })
|
||||
}
|
||||
|
||||
export function getInventory(user_id, page = 1, page_size = 20){
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size } })
|
||||
export function getInventory(user_id, page = 1, page_size = 20) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
|
||||
export function bindPhone(user_id, code, extraHeader = {}) {
|
||||
@ -87,8 +87,10 @@ export function requestShipping(user_id, ids) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data: { inventory_ids: ids } })
|
||||
}
|
||||
|
||||
export function getItemCards(user_id) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/item_cards`, method: 'GET' })
|
||||
export function getItemCards(user_id, status) {
|
||||
const data = {}
|
||||
if (status !== undefined) data.status = status
|
||||
return authRequest({ url: `/api/app/users/${user_id}/item_cards`, method: 'GET', data })
|
||||
}
|
||||
|
||||
export function getUserCoupons(user_id, status, page = 1, page_size = 20) {
|
||||
@ -97,6 +99,18 @@ export function getUserCoupons(user_id, status, page = 1, page_size = 20) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/coupons`, method: 'GET', data })
|
||||
}
|
||||
|
||||
export function getCouponStats(user_id) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/coupons/stats`, method: 'GET' })
|
||||
}
|
||||
|
||||
export function getCouponUsage(user_id, user_coupon_id, page = 1, page_size = 20) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/coupons/${user_coupon_id}/usage`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
|
||||
export function redeemCouponByPoints(user_id, coupon_id) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-coupon`, method: 'POST', data: { coupon_id } })
|
||||
}
|
||||
|
||||
export function redeemCoupon(user_id, code) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/coupons/redeem`, method: 'POST', data: { code } })
|
||||
}
|
||||
@ -128,3 +142,24 @@ export function getTasks(page = 1, page_size = 20) {
|
||||
export function getShipments(user_id, page = 1, page_size = 20) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/shipments`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
|
||||
// 获取用户邀请记录
|
||||
export function getUserInvites(user_id, page = 1, page_size = 20) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/invites`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 兼容性适配接口 (适配 pages/mine/index.vue)
|
||||
// ============================================
|
||||
|
||||
export function getUserInfo() {
|
||||
const user_info = uni.getStorageSync('user_info')
|
||||
if (user_info) return Promise.resolve(user_info)
|
||||
return authRequest({ url: '/api/app/users/info', method: 'GET' })
|
||||
}
|
||||
|
||||
export const getUserTasks = getTasks
|
||||
export function getInviteRecords(page = 1, page_size = 20) {
|
||||
const user_id = uni.getStorageSync('user_id')
|
||||
return getUserInvites(user_id, page, page_size)
|
||||
}
|
||||
|
||||
@ -20,11 +20,36 @@ const shadowClass = computed(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ep-card { background: #fff; border-radius: 12rpx; overflow: hidden; border: 1rpx solid #eee }
|
||||
.is-always-shadow { box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06) }
|
||||
.is-hover-shadow { box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06) }
|
||||
.is-no-shadow { box-shadow: none }
|
||||
.ep-card__header { padding: 12rpx 16rpx; border-bottom: 1rpx solid #f0f0f0 }
|
||||
.ep-card__body { padding: 12rpx 16rpx }
|
||||
<style lang="scss" scoped>
|
||||
.ep-card {
|
||||
background: $bg-card;
|
||||
border: 1rpx solid $border-color-light;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.is-always-shadow {
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.is-hover-shadow {
|
||||
/* 在移动端hover效果不如PC明显,这里可以处理为点击态 */
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.is-no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ep-card__header {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border-bottom: 1rpx solid $border-color-light;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.ep-card__body {
|
||||
padding: $spacing-md;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({ rewards: { type: Array, default: () => [] }, controls: { type: Boolean, default: true } })
|
||||
const emit = defineEmits(['draw'])
|
||||
@ -64,20 +64,143 @@ function onPreview(cell) {
|
||||
defineExpose({ revealResults, reset })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flip-root { display: flex; flex-direction: column; gap: 16rpx; padding: 16rpx }
|
||||
.flip-actions { display: flex; gap: 12rpx }
|
||||
.flip-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
|
||||
.flip-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12rpx }
|
||||
.flip-card { perspective: 1000px }
|
||||
.flip-inner { position: relative; width: 100%; height: 200rpx; transform-style: preserve-3d; transition: transform 0.5s }
|
||||
.flip-card.flipped .flip-inner { transform: rotateY(180deg) }
|
||||
.flip-front, .flip-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 12rpx; overflow: hidden }
|
||||
.flip-front { background: #e2e8f0; display: flex; align-items: center; justify-content: center }
|
||||
.front-placeholder { width: 80%; height: 80%; border-radius: 12rpx; background: linear-gradient(135deg, #f8fafc, #e2e8f0) }
|
||||
.flip-back { background: #fff; transform: rotateY(180deg); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 12rpx }
|
||||
.flip-image { width: 80%; border-radius: 8rpx; margin-bottom: 8rpx; background: #f5f5f5 }
|
||||
.flip-title { font-size: 26rpx; color: #222; text-align: center; max-width: 90%; word-break: break-all }
|
||||
.flip-toolbar { display: flex; justify-content: flex-end }
|
||||
.flip-reset { background: #ffd166; color: #6b4b1f; border-radius: 999rpx }
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 翻牌动画组件
|
||||
采用暖橙色调的开箱效果
|
||||
============================================ */
|
||||
|
||||
.flip-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-md;
|
||||
}
|
||||
|
||||
.flip-actions {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
.flip-btn {
|
||||
flex: 1;
|
||||
background: $gradient-brand !important;
|
||||
color: #FFFFFF !important;
|
||||
border-radius: $radius-md;
|
||||
font-weight: 600;
|
||||
box-shadow: $shadow-md;
|
||||
border: none;
|
||||
font-size: $font-md;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.flip-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: $spacing-sm;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-sm;
|
||||
box-shadow: inset 0 0 20rpx rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.flip-card {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.flip-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.flip-card.flipped .flip-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-front, .flip-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
border-radius: $radius-md;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flip-front {
|
||||
background: linear-gradient(145deg, #FFF8F3, #FFE8D1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid rgba($brand-primary, 0.2);
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.front-placeholder {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
border-radius: $radius-md;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.3), rgba($brand-primary-light, 0.2));
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.6; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.flip-back {
|
||||
background: $bg-card;
|
||||
transform: rotateY(180deg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-sm;
|
||||
border: 2rpx solid rgba($brand-primary, 0.3);
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
.flip-image {
|
||||
width: 75%;
|
||||
border-radius: $radius-sm;
|
||||
margin-bottom: $spacing-xs;
|
||||
background: linear-gradient(145deg, #FFF8F3, #FFF4E6);
|
||||
}
|
||||
|
||||
.flip-title {
|
||||
font-size: $font-xs;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.flip-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flip-reset {
|
||||
background: linear-gradient(135deg, $accent-gold, $brand-primary-light) !important;
|
||||
color: #6b4b1f !important;
|
||||
border-radius: 999rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: $shadow-sm;
|
||||
font-size: $font-sm;
|
||||
padding: 0 40rpx;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -133,14 +133,19 @@ function handleConfirm() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 支付弹窗组件
|
||||
采用暖橙色调的底部弹窗设计
|
||||
============================================ */
|
||||
|
||||
.payment-popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@ -148,124 +153,168 @@ function handleConfirm() {
|
||||
|
||||
.payment-popup-content {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding: 30rpx;
|
||||
padding-bottom: calc(30rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
||||
background: $bg-card;
|
||||
border-radius: $radius-xl $radius-xl 0 0;
|
||||
padding: $spacing-lg;
|
||||
padding-bottom: calc($spacing-lg + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
|
||||
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.risk-warning {
|
||||
background-color: #fffbe6;
|
||||
color: #ed6a0c;
|
||||
font-size: 24rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: linear-gradient(135deg, #FFF8F3, #FFF4E6);
|
||||
color: #B45309;
|
||||
font-size: $font-xs;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border-radius: $radius-md;
|
||||
margin-bottom: $spacing-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
border: 1rpx solid rgba(255, 159, 67, 0.2);
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: #1890ff;
|
||||
text-decoration: underline;
|
||||
color: $brand-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
margin-bottom: $spacing-lg;
|
||||
position: relative;
|
||||
}
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
font-size: $font-lg;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
}
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 48rpx;
|
||||
color: $text-placeholder;
|
||||
line-height: 1;
|
||||
padding: 10rpx;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.close-icon:active {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
padding: 30rpx;
|
||||
padding: $spacing-sm 0 $spacing-md;
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
margin-bottom: $spacing-xl;
|
||||
padding: $spacing-md;
|
||||
background: linear-gradient(145deg, #FFFFFF, #FFF8F3);
|
||||
border-radius: $radius-lg;
|
||||
border: 1rpx solid rgba(255, 159, 67, 0.1);
|
||||
}
|
||||
.amount-section .label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
font-size: $font-sm;
|
||||
color: $text-secondary;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
.amount-section .amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #ff4d4f;
|
||||
font-size: 56rpx;
|
||||
font-weight: 800;
|
||||
background: $gradient-brand;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
.form-item .label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: $font-sm;
|
||||
color: $text-main;
|
||||
font-weight: 600;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid $border-color-light;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
background: #f9f9f9;
|
||||
font-size: $font-sm;
|
||||
background: $bg-page;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.picker-display:active {
|
||||
border-color: $brand-primary;
|
||||
background: #FFF8F3;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
color: #333;
|
||||
color: $text-main;
|
||||
font-weight: 500;
|
||||
}
|
||||
.placeholder {
|
||||
color: #999;
|
||||
color: $text-placeholder;
|
||||
}
|
||||
.arrow {
|
||||
color: #ccc;
|
||||
color: $text-placeholder;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-right: 2rpx solid #ccc;
|
||||
border-bottom: 2rpx solid #ccc;
|
||||
border-right: 3rpx solid $text-placeholder;
|
||||
border-bottom: 3rpx solid $text-placeholder;
|
||||
transform: rotate(-45deg);
|
||||
margin-right: 8rpx;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #eee;
|
||||
gap: $spacing-md;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
.btn-cancel, .btn-confirm {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: #fff;
|
||||
border-radius: 0;
|
||||
font-size: 30rpx;
|
||||
padding: 24rpx 0;
|
||||
border-radius: $radius-lg;
|
||||
font-size: $font-md;
|
||||
padding: $spacing-md 0;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-cancel::after, .btn-confirm::after {
|
||||
border: none;
|
||||
}
|
||||
.btn-cancel {
|
||||
color: #666;
|
||||
border-right: 1rpx solid #eee;
|
||||
color: $text-secondary;
|
||||
background: #F3F4F6;
|
||||
}
|
||||
.btn-cancel:active {
|
||||
background: $border-color-light;
|
||||
}
|
||||
.btn-confirm {
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
background: $gradient-brand;
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
.btn-confirm:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
</view>
|
||||
|
||||
<view class="action-buttons">
|
||||
<button v-if="selectedItems.length === 0" class="btn-random" @tap="handleRandomOne">随机一发</button>
|
||||
<button v-else class="btn-buy" @tap="handleBuy">去支付</button>
|
||||
<button v-if="selectedItems.length === 0" class="btn-common btn-random" @tap="handleRandomOne">随机一发</button>
|
||||
<button v-else class="btn-common btn-buy" @tap="handleBuy">去支付</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -290,122 +290,255 @@ async function onPaymentConfirm(paymentData) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 选号组件 (适配高级卡片布局)
|
||||
============================================ */
|
||||
|
||||
/* 容器 - 去除背景,融入父级卡片 */
|
||||
.choice-grid-container {
|
||||
padding: 20rpx;
|
||||
padding: $spacing-xs 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 加载和空状态 */
|
||||
.loading-state, .empty-state {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
padding: 80rpx 0;
|
||||
color: $text-placeholder;
|
||||
font-size: $font-sm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
&::before {
|
||||
content: '📦';
|
||||
font-size: 60rpx;
|
||||
opacity: 0.5;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* 网格包装 */
|
||||
.grid-wrapper {
|
||||
padding-bottom: 200rpx; /* 留出底部操作栏空间 */
|
||||
padding: 0 20rpx 200rpx;
|
||||
}
|
||||
|
||||
/* 号码网格 - 调整为更合理的列数,适配不同屏幕 */
|
||||
.choices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr); /* 一行5个 */
|
||||
grid-template-columns: repeat(6, 1fr); /* 默认6列,更大尺寸 */
|
||||
gap: 16rpx;
|
||||
margin-bottom: 120rpx; /* 留出底部操作栏空间 */
|
||||
padding: 10rpx 0;
|
||||
animation: scaleIn 0.4s ease-out backwards;
|
||||
}
|
||||
|
||||
/* 单个号码格子 */
|
||||
.choice-item {
|
||||
aspect-ratio: 1;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid $border-color-light;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
/* 票据孔装饰 */
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: $bg-page; /* 与背景色一致 */
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
&::before { left: -4rpx; }
|
||||
&::after { right: -4rpx; }
|
||||
}
|
||||
|
||||
.choice-item:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
/* 号码文字 */
|
||||
.choice-number {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: $text-sub;
|
||||
z-index: 1;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
/* 状态文字 - 简化为小点或隐藏 */
|
||||
.choice-status {
|
||||
font-size: 20rpx;
|
||||
margin-top: 4rpx;
|
||||
color: #666;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 状态样式 */
|
||||
/* ============= 状态样式 ============= */
|
||||
|
||||
/* 可选状态 */
|
||||
.is-available {
|
||||
background: #fff;
|
||||
background: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
border-color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
/* 已售状态 */
|
||||
.is-sold {
|
||||
background: #f5f5f5;
|
||||
border-color: #eee;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.is-sold .choice-number {
|
||||
color: #ccc;
|
||||
text-decoration: line-through;
|
||||
color: $text-disabled;
|
||||
border-color: $border-color-light;
|
||||
background: $bg-secondary;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: 'SOLD';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-30deg);
|
||||
font-size: 20rpx;
|
||||
font-weight: 900;
|
||||
color: $text-disabled;
|
||||
opacity: 0.2;
|
||||
letter-spacing: 2rpx;
|
||||
border: 2rpx solid $text-disabled;
|
||||
padding: 2rpx 6rpx;
|
||||
border-radius: 4rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-sold .choice-number {
|
||||
color: $text-disabled;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 选中状态 - 橙色高亮 */
|
||||
.is-selected {
|
||||
background: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
background: $gradient-brand;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.3);
|
||||
transform: scale(1.08);
|
||||
z-index: 2;
|
||||
|
||||
&::before, &::after {
|
||||
display: none; /* 选中状态隐藏票据孔 */
|
||||
}
|
||||
}
|
||||
.is-selected .choice-number {
|
||||
color: #1890ff;
|
||||
}
|
||||
.is-selected .choice-status {
|
||||
color: #1890ff;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
/* ============= 底部操作栏 ============= */
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
left: 30rpx;
|
||||
right: 30rpx;
|
||||
background: rgba($bg-card, 0.9);
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 20rpx 30rpx;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
|
||||
box-shadow: $shadow-lg;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
border: 1rpx solid rgba($bg-card, 0.5);
|
||||
animation: slideUp 0.4s ease-out backwards;
|
||||
}
|
||||
|
||||
/* 选择信息行 */
|
||||
.selection-info {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
color: $text-main;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #ff4d4f;
|
||||
font-weight: bold;
|
||||
margin: 0 4rpx;
|
||||
color: $brand-primary;
|
||||
font-weight: 800;
|
||||
font-size: 36rpx;
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn-common {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
padding: 0 48rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
/* 购买按钮 */
|
||||
.btn-buy {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 0 60rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
font-size: 30rpx;
|
||||
margin: 0;
|
||||
background: $gradient-brand !important;
|
||||
color: #FFFFFF !important;
|
||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.3);
|
||||
|
||||
/* 脉冲动画 */
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 随机按钮 */
|
||||
.btn-random {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 0 60rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
font-size: 30rpx;
|
||||
margin: 0;
|
||||
background: $bg-secondary !important;
|
||||
color: $text-main !important;
|
||||
box-shadow: none;
|
||||
border: 1rpx solid transparent;
|
||||
|
||||
&:active {
|
||||
background: #E5E7EB !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba($brand-primary, 0.4); }
|
||||
70% { box-shadow: 0 0 0 20rpx rgba($brand-primary, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba($brand-primary, 0); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10rpx); }
|
||||
}
|
||||
</style>
|
||||
|
||||
99
pages.json
99
pages.json
@ -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": {}
|
||||
}
|
||||
}
|
||||
@ -23,51 +23,60 @@
|
||||
</view>
|
||||
|
||||
<view v-show="tabActive === 'pool'">
|
||||
<view class="rewards" v-if="currentIssueId && rewardsMap[currentIssueId] && rewardsMap[currentIssueId].length">
|
||||
<el-card v-for="rw in rewardsMap[currentIssueId]" :key="rw.id" class="el-reward-card" shadow="hover">
|
||||
<template #header>
|
||||
<view class="el-card-header">
|
||||
<text class="el-card-title">{{ rw.title }}</text>
|
||||
<text v-if="rw.boss" class="badge-boss">BOSS</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="card-image-wrap">
|
||||
<image v-if="rw.image" class="card-image" :src="rw.image" mode="widthFix" />
|
||||
<text class="prob-corner tl">概率 {{ rw.percent }}%</text>
|
||||
<view class="rewards-grid" v-if="currentIssueId && rewardsMap[currentIssueId] && rewardsMap[currentIssueId].length">
|
||||
<view v-for="(rw, idx) in rewardsMap[currentIssueId]" :key="rw.id"
|
||||
class="reward-card animate-stagger"
|
||||
:style="{ '--delay': idx * 0.05 + 's' }">
|
||||
<view class="card-header">
|
||||
<text class="card-title">{{ rw.title }}</text>
|
||||
<text v-if="rw.boss" class="badge-boss">BOSS</text>
|
||||
</view>
|
||||
</el-card>
|
||||
<view class="image-wrapper">
|
||||
<image v-if="rw.image" class="reward-image" :src="rw.image" mode="aspectFill" />
|
||||
<text class="prob-tag absolute-tag">概率 {{ rw.percent }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" v-else>
|
||||
<text class="empty-icon">📭</text>
|
||||
<text class="empty-text">暂无奖励配置</text>
|
||||
</view>
|
||||
<view class="rewards-empty" v-else>暂无奖励配置</view>
|
||||
</view>
|
||||
|
||||
<view v-show="tabActive === 'records'">
|
||||
<view class="records" v-if="winRecords.length">
|
||||
<el-card v-for="it in winRecords" :key="it.id" class="el-reward-card" shadow="hover">
|
||||
<template #header>
|
||||
<view class="el-card-header">
|
||||
<text class="el-card-title">{{ it.title }}</text>
|
||||
<text v-if="it.count !== undefined" class="badge-count">x{{ it.count }}</text>
|
||||
<view class="records-list" v-if="winRecords.length">
|
||||
<view v-for="(it, idx) in winRecords" :key="it.id"
|
||||
class="record-item animate-stagger"
|
||||
:style="{ '--delay': idx * 0.05 + 's' }">
|
||||
<image class="record-img" :src="it.image" mode="aspectFill" />
|
||||
<view class="record-info">
|
||||
<view class="record-title">{{ it.title }}</view>
|
||||
<view class="record-meta">
|
||||
<text class="record-count">x{{ it.count }}</text>
|
||||
<text v-if="it.percent !== undefined">占比 {{ it.percent }}%</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="card-image-wrap">
|
||||
<image v-if="it.image" class="card-image" :src="it.image" mode="widthFix" />
|
||||
<text v-if="it.percent !== undefined" class="prob-corner tl">占比 {{ it.percent }}%</text>
|
||||
</view>
|
||||
</el-card>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" v-else>
|
||||
<text class="empty-icon">📝</text>
|
||||
<text class="empty-text">暂无中奖记录</text>
|
||||
</view>
|
||||
<view class="rewards-empty" v-else>暂无中奖记录</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="issues-empty">暂无期数</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="float-actions">
|
||||
<button class="float-btn primary" @click="onParticipate">立即参与</button>
|
||||
<view class="float-bar">
|
||||
<button class="action-btn primary" @click="onParticipate">
|
||||
立即参与
|
||||
<view class="btn-shine"></view>
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import ElCard from '../../../components/ElCard.vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue, getActivityWinRecords } from '../../../api/appUser'
|
||||
@ -241,21 +250,6 @@ function onIssueChange(e) {
|
||||
currentIssueId.value = (cur && cur.id) || ''
|
||||
}
|
||||
|
||||
async function ensureElCard() {
|
||||
const inst = getCurrentInstance()
|
||||
const app = inst && inst.appContext && inst.appContext.app
|
||||
let comp = null
|
||||
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
|
||||
comp = window.ElementPlus.ElCard
|
||||
} else {
|
||||
comp = ElCard
|
||||
}
|
||||
if (app && !app.component('el-card')) app.component('el-card', comp)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function onPreviewBanner() {
|
||||
const url = detail.value.banner || ''
|
||||
if (url) uni.previewImage({ urls: [url], current: url })
|
||||
@ -292,40 +286,369 @@ onLoad((opts) => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { height: 100vh; padding-bottom: 140rpx }
|
||||
.banner { padding: 24rpx }
|
||||
.banner-img { width: 100% }
|
||||
.header { padding: 0 24rpx }
|
||||
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
|
||||
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||||
.btn { flex: 1 }
|
||||
.primary { background-color: #007AFF; color: #fff }
|
||||
.float-actions { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: rgba(255,255,255,0.9); box-shadow: 0 -6rpx 16rpx rgba(0,0,0,0.06); z-index: 9999 }
|
||||
.float-btn { width: 100%; border-radius: 999rpx }
|
||||
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||||
.issues-list { }
|
||||
.issue-picker { height: 200rpx; background: #f8f8f8; border-radius: 12rpx; margin-bottom: 64rpx }
|
||||
.picker-item { height: 40rpx; line-height: 40rpx; text-align: center; font-size: 26rpx }
|
||||
.tabs { display: flex; padding: 0 12rpx; margin-bottom: 16rpx }
|
||||
.tab { flex: 1; text-align: center; font-size: 28rpx; padding: 16rpx 0; border: 2rpx solid #f0c58a; color: #8a5a2b; background: #fff3df; border-radius: 16rpx }
|
||||
.tab + .tab { margin-left: 12rpx }
|
||||
.tab.active { background: #ffdfaa; border-color: #ffb74d; color: #6b4b1f; font-weight: 600 }
|
||||
.rewards { width: 100%; margin-top: 24rpx }
|
||||
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||||
.el-reward-card { margin-bottom: 12rpx }
|
||||
.el-card-header { display: flex; align-items: center; justify-content: space-between }
|
||||
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||||
.card-image-wrap { position: relative; padding-bottom: 48rpx }
|
||||
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
|
||||
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
|
||||
.prob-corner.tl { top: 12rpx; left: 12rpx }
|
||||
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
|
||||
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||||
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.badge-count { background: #ffd166; color: #6b4b1f; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||
.issues-empty { font-size: 24rpx; color: #999 }
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
对对碰活动页面 - 高级设计重构 (SCSS Integration)
|
||||
============================================ */
|
||||
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80rpx);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.orb-1 {
|
||||
width: 500rpx; height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.2) 0%, transparent 70%);
|
||||
top: -100rpx; left: -100rpx;
|
||||
}
|
||||
.orb-2 {
|
||||
width: 600rpx; height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($accent-gold, 0.15) 0%, transparent 70%);
|
||||
bottom: -100rpx; right: -100rpx;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* Banner */
|
||||
.banner-wrapper {
|
||||
margin: $spacing-md $spacing-lg;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-lg;
|
||||
position: relative;
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
.banner-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.banner-shadow {
|
||||
position: absolute;
|
||||
bottom: 0; left: 0; width: 100%; height: 40%;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header-section {
|
||||
padding: 0 $spacing-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
text-align: center;
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
.title-row {
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
.title-text {
|
||||
font-size: $font-xxl;
|
||||
font-weight: 900;
|
||||
background: $gradient-brand;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
.price-tag {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
background: rgba($bg-card, 0.6);
|
||||
padding: $spacing-xs $spacing-lg;
|
||||
border-radius: $radius-round;
|
||||
backdrop-filter: blur(20rpx);
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
.price-label { font-size: $font-sm; color: $text-sub; margin-right: $spacing-xs; }
|
||||
.price-symbol { font-size: $font-sm; color: $brand-primary; font-weight: 700; }
|
||||
.price-value { font-size: $font-xl; color: $brand-primary; font-weight: 900; font-family: 'DIN Alternate', sans-serif; }
|
||||
|
||||
/* Glass Card */
|
||||
.glass-card {
|
||||
margin: 0 $spacing-lg $spacing-lg;
|
||||
background: rgba($bg-card, 0.8);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border-radius: $radius-xl;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
animation: fadeInUp 0.6s ease-out 0.2s backwards;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-md;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: $font-lg;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
position: relative;
|
||||
padding-left: 20rpx;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 50%; transform: translateY(-50%);
|
||||
width: 8rpx; height: 32rpx;
|
||||
background: $gradient-brand;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
}
|
||||
.issue-indicator {
|
||||
font-size: $font-sm;
|
||||
color: $brand-primary;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
padding: 4rpx $spacing-md;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Custom Picker */
|
||||
.custom-picker {
|
||||
height: 280rpx;
|
||||
background: rgba($bg-secondary, 0.5);
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
overflow: hidden;
|
||||
}
|
||||
.picker-item {
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
.picker-text { font-size: $font-lg; color: $text-main; font-weight: 600; }
|
||||
.picker-status {
|
||||
font-size: $font-xs; color: $text-sub; background: rgba(0,0,0,0.05); padding: 2rpx $spacing-sm; border-radius: $radius-sm;
|
||||
|
||||
&.status-active { background: #D1FAE5; color: #059669; }
|
||||
}
|
||||
|
||||
/* Modern Tabs */
|
||||
.modern-tabs {
|
||||
display: flex;
|
||||
background: $bg-secondary;
|
||||
padding: 8rpx;
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: $spacing-md 0;
|
||||
font-size: $font-md;
|
||||
color: $text-sub;
|
||||
border-radius: $radius-md;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background: #FFFFFF;
|
||||
color: $brand-primary;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
}
|
||||
.active-dot {
|
||||
width: 8rpx; height: 8rpx;
|
||||
background: $brand-primary;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
bottom: 8rpx; left: 50%; transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Rewards Grid */
|
||||
.rewards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
.reward-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
border: 1rpx solid rgba(0,0,0,0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-md;
|
||||
height: 44rpx;
|
||||
}
|
||||
.card-title {
|
||||
font-size: $font-md;
|
||||
color: $text-main;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.badge-boss {
|
||||
font-size: $font-xs;
|
||||
background: $gradient-gold;
|
||||
color: #78350F;
|
||||
padding: 2rpx $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
background: $bg-secondary;
|
||||
border-radius: $radius-md;
|
||||
overflow: hidden;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
.reward-image {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
}
|
||||
.prob-tag {
|
||||
position: absolute;
|
||||
top: 8rpx; left: 8rpx;
|
||||
font-size: $font-xs;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(4rpx);
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Records List */
|
||||
.records-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
.record-item {
|
||||
display: flex;
|
||||
background: #FFFFFF;
|
||||
padding: $spacing-lg;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
align-items: center;
|
||||
}
|
||||
.record-img {
|
||||
width: 100rpx; height: 100rpx;
|
||||
border-radius: $radius-md;
|
||||
background: $bg-secondary;
|
||||
margin-right: $spacing-lg;
|
||||
}
|
||||
.record-info {
|
||||
flex: 1;
|
||||
}
|
||||
.record-title {
|
||||
font-size: $font-md;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
.record-meta {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
.record-count {
|
||||
background: rgba($brand-primary, 0.1);
|
||||
color: $brand-primary;
|
||||
padding: 2rpx $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
color: $text-placeholder;
|
||||
}
|
||||
.empty-icon { font-size: 80rpx; margin-bottom: $spacing-lg; opacity: 0.5; }
|
||||
.empty-text { font-size: $font-md; }
|
||||
|
||||
/* Float Bar */
|
||||
.float-bar {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: $spacing-lg $spacing-xl;
|
||||
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 -8rpx 30rpx rgba(0, 0, 0, 0.05);
|
||||
z-index: 100;
|
||||
animation: slideUp 0.4s ease-out backwards;
|
||||
}
|
||||
.action-btn {
|
||||
height: 96rpx;
|
||||
border-radius: $radius-round;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-xl;
|
||||
font-weight: 800;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.primary {
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
box-shadow: $shadow-warm;
|
||||
}
|
||||
|
||||
&:active { transform: scale(0.98); }
|
||||
}
|
||||
.btn-shine {
|
||||
position: absolute;
|
||||
top: 0; left: -100%; width: 50%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transform: skewX(-20deg);
|
||||
animation: shine 3s infinite;
|
||||
}
|
||||
|
||||
/* Animation Utilities */
|
||||
.animate-stagger {
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
animation-delay: var(--delay, 0s);
|
||||
}
|
||||
</style>
|
||||
|
||||
302
pages/activity/list/index.vue
Normal file
302
pages/activity/list/index.vue
Normal file
@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view class="content" scroll-y>
|
||||
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
|
||||
<view v-else-if="filteredActivities.length > 0" class="activity-grid">
|
||||
<view class="activity-item" v-for="a in filteredActivities" :key="a.id" @tap="onActivityTap(a)">
|
||||
<view class="thumb-box">
|
||||
<image class="thumb" :src="a.image" mode="aspectFill" />
|
||||
<view class="tag-hot">HOT</view>
|
||||
</view>
|
||||
<view class="info">
|
||||
<view class="name">{{ a.title }}</view>
|
||||
<view class="bottom-row">
|
||||
<text class="price-text">{{ a.category_name }} · {{ a.subtitle }}</text>
|
||||
<view class="btn-go">GO</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">
|
||||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||||
<text class="empty-text">暂无{{ title }}活动</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { request, authRequest } from '@/utils/request.js'
|
||||
|
||||
const title = ref('')
|
||||
const categoryTarget = ref('')
|
||||
const activities = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const filteredActivities = computed(() => {
|
||||
if (!categoryTarget.value) return activities.value
|
||||
const target = categoryTarget.value.trim()
|
||||
return activities.value.filter(a => {
|
||||
const cat = (a.category_name || '').trim()
|
||||
return cat === target || cat.includes(target)
|
||||
})
|
||||
})
|
||||
|
||||
function apiGet(url) {
|
||||
const token = uni.getStorageSync('token')
|
||||
const fn = token ? authRequest : request
|
||||
return fn({ url })
|
||||
}
|
||||
|
||||
function cleanUrl(u) {
|
||||
const s = String(u || '').trim()
|
||||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||||
if (m && m[0]) return m[0]
|
||||
return s.replace(/[`'\"]/g, '').trim()
|
||||
}
|
||||
|
||||
function buildSubtitle(i) {
|
||||
const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? ''
|
||||
if (base) return base
|
||||
const price = (i.price_draw !== undefined && i.price_draw !== null) ? `¥${(Number(i.price_draw || 0) / 100).toFixed(2)}` : ''
|
||||
return price
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await apiGet('/api/app/activities')
|
||||
let list = []
|
||||
if (Array.isArray(res)) list = res
|
||||
else if (res && (Array.isArray(res.list) || Array.isArray(res.data))) list = res.list || res.data
|
||||
|
||||
activities.value = list.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
image: cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
|
||||
title: (i.title ?? i.name ?? '').replace(/无限赏|一番赏|对对碰|爬塔/g, '').trim(),
|
||||
subtitle: buildSubtitle(i),
|
||||
category_name: i.category_name ?? i.categoryName ?? '',
|
||||
link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '')
|
||||
})).filter(i => i.image || i.title)
|
||||
|
||||
} catch (e) {
|
||||
activities.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onActivityTap(a) {
|
||||
const name = (a.category_name || '').trim()
|
||||
const id = a.id
|
||||
let path = ''
|
||||
|
||||
// Navigate to DETAIL, not list
|
||||
if (name.includes('一番赏')) path = '/pages/activity/yifanshang/index'
|
||||
else if (name.includes('无限赏')) path = '/pages/activity/wuxianshang/index'
|
||||
else if (name.includes('对对碰')) path = '/pages/activity/duiduipeng/index'
|
||||
else if (name.includes('爬塔')) path = '/pages/activity/pata/index'
|
||||
|
||||
if (path && id) {
|
||||
uni.navigateTo({ url: `${path}?id=${id}` })
|
||||
return
|
||||
}
|
||||
if (a.link && /^\/.+/.test(a.link)) {
|
||||
uni.navigateTo({ url: a.link })
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
if (opts && opts.category) {
|
||||
categoryTarget.value = decodeURIComponent(opts.category)
|
||||
title.value = categoryTarget.value
|
||||
uni.setNavigationBarTitle({ title: categoryTarget.value })
|
||||
}
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 分享功能
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: `${title.value || '精彩活动'} - 奇盒潮玩`,
|
||||
path: `/pages/index/index?invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
})
|
||||
|
||||
onShareTimeline(() => {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: `${title.value || '精彩活动'} - 奇盒潮玩`,
|
||||
query: `invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
.activity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-md;
|
||||
}
|
||||
.activity-item {
|
||||
background: #fff;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-sm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
position: relative;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: $shadow-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%; /* 1:1 Aspect Ratio */
|
||||
height: 0;
|
||||
background: $bg-secondary;
|
||||
}
|
||||
.thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: linear-gradient(90deg, $bg-secondary 25%, #e8e8e8 50%, $bg-secondary 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
.tag-hot {
|
||||
position: absolute;
|
||||
top: 16rpx; left: 16rpx;
|
||||
background: rgba(255, 69, 58, 0.9);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 69, 58, 0.3);
|
||||
backdrop-filter: blur(4rpx);
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 20rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 16rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
height: 80rpx;
|
||||
}
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.price-text {
|
||||
font-size: 22rpx;
|
||||
color: $text-secondary;
|
||||
max-width: 70%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.btn-go {
|
||||
background: $brand-primary;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 900;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 100rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background: darken($brand-primary, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
display: flex; justify-content: center; padding: 100rpx;
|
||||
}
|
||||
.spinner {
|
||||
width: 48rpx; height: 48rpx;
|
||||
border: 4rpx solid $border-color-light;
|
||||
border-top-color: $brand-primary;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 200rpx;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
.empty-img {
|
||||
width: 240rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.empty-text {
|
||||
color: $text-secondary;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
动画增强
|
||||
============================================ */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* 卡片交错入场 */
|
||||
@for $i from 1 through 10 {
|
||||
.activity-item:nth-child(#{$i}) {
|
||||
animation: fadeInUp 0.4s ease-out #{$i * 0.05}s both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
573
pages/activity/pata/index.vue
Normal file
573
pages/activity/pata/index.vue
Normal file
@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<view class="page-wrapper">
|
||||
<!-- Rebuild Trigger -->
|
||||
<!-- 背景层 -->
|
||||
<image class="bg-fixed" :src="detail.banner || ''" mode="aspectFill" />
|
||||
<view class="bg-mask"></view>
|
||||
|
||||
<view class="content-area">
|
||||
<!-- 顶部信息 -->
|
||||
<view class="header-section">
|
||||
<view class="title-box">
|
||||
<text class="main-title">{{ detail.name || detail.title || '爬塔挑战' }}</text>
|
||||
<text class="sub-title">层层突围 赢取大奖</text>
|
||||
</view>
|
||||
<view class="rule-btn" @tap="showRules">规则</view>
|
||||
</view>
|
||||
|
||||
<!-- 挑战区域 (模拟塔层) -->
|
||||
<view class="tower-container">
|
||||
<view class="tower-level current">
|
||||
<view class="level-info">
|
||||
<text class="level-num">当前挑战</text>
|
||||
<text class="level-name">{{ currentIssueTitle || '第1层' }}</text>
|
||||
</view>
|
||||
<view class="level-status">进行中</view>
|
||||
</view>
|
||||
|
||||
<!-- 奖池预览 -->
|
||||
<view class="rewards-preview" v-if="currentIssueRewards.length">
|
||||
<scroll-view scroll-x class="rewards-scroll">
|
||||
<view class="reward-item" v-for="(r, idx) in currentIssueRewards" :key="idx">
|
||||
<image class="reward-img" :src="r.image" mode="aspectFill" />
|
||||
<view class="reward-name">{{ r.title }}</view>
|
||||
<view class="reward-prob" v-if="r.percent">概率 {{ r.percent }}%</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<view class="action-area">
|
||||
<view class="price-display">
|
||||
<text class="currency">¥</text>
|
||||
<text class="amount">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
||||
<text class="unit">/次</text>
|
||||
</view>
|
||||
<button class="challenge-btn" :loading="drawLoading" @tap="onStartChallenge">
|
||||
立即挑战
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 结果弹窗 -->
|
||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||||
<view class="flip-mask" @tap="closeFlip"></view>
|
||||
<view class="flip-content">
|
||||
<FlipGrid ref="flipRef" :rewards="winItems" :controls="false" />
|
||||
<button class="close-btn" @tap="closeFlip">收下奖励</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<PaymentPopup
|
||||
v-model:visible="paymentVisible"
|
||||
:amount="paymentAmount"
|
||||
:coupons="coupons"
|
||||
:propCards="propCards"
|
||||
@confirm="onPaymentConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import FlipGrid from '../../../components/FlipGrid.vue'
|
||||
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '../../../api/appUser'
|
||||
|
||||
const activityId = ref('')
|
||||
const detail = ref({})
|
||||
const issues = ref([])
|
||||
const currentIssueId = ref('')
|
||||
const rewardsMap = ref({})
|
||||
const drawLoading = ref(false)
|
||||
const showFlip = ref(false)
|
||||
const winItems = ref([])
|
||||
const flipRef = ref(null)
|
||||
|
||||
// Payment
|
||||
const paymentVisible = ref(false)
|
||||
const paymentAmount = ref('0.00')
|
||||
const coupons = ref([])
|
||||
const propCards = ref([])
|
||||
const selectedCoupon = ref(null)
|
||||
const selectedCard = ref(null)
|
||||
const pendingCount = ref(1)
|
||||
|
||||
const currentIssueTitle = computed(() => {
|
||||
const i = issues.value.find(x => x.id === currentIssueId.value)
|
||||
return i ? (i.title || `第${i.no}期`) : ''
|
||||
})
|
||||
|
||||
const currentIssueRewards = computed(() => {
|
||||
return (currentIssueId.value && rewardsMap.value[currentIssueId.value]) || []
|
||||
})
|
||||
|
||||
const priceVal = computed(() => Number(detail.value.price_draw || 0) / 100)
|
||||
|
||||
async function loadData(id) {
|
||||
try {
|
||||
const d = await getActivityDetail(id)
|
||||
detail.value = d || {}
|
||||
|
||||
const is = await getActivityIssues(id)
|
||||
issues.value = normalizeIssues(is)
|
||||
|
||||
if (issues.value.length) {
|
||||
const first = issues.value[0]
|
||||
currentIssueId.value = first.id
|
||||
loadRewards(id, first.id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRewards(aid, iid) {
|
||||
try {
|
||||
const res = await getActivityIssueRewards(aid, iid)
|
||||
rewardsMap.value[iid] = normalizeRewards(res)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function onStartChallenge() {
|
||||
const token = uni.getStorageSync('token')
|
||||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||||
if (!token || !phoneBound) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
// In real app, redirect to login
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentIssueId.value) {
|
||||
uni.showToast({ title: '暂无挑战场次', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
paymentAmount.value = priceVal.value.toFixed(2)
|
||||
pendingCount.value = 1
|
||||
paymentVisible.value = true
|
||||
// Fetch coupons/cards in background
|
||||
fetchPropCards()
|
||||
fetchCoupons()
|
||||
}
|
||||
|
||||
async function onPaymentConfirm(data) {
|
||||
selectedCoupon.value = data?.coupon || null
|
||||
selectedCard.value = data?.card || null
|
||||
paymentVisible.value = false
|
||||
await doDraw()
|
||||
}
|
||||
|
||||
async function doDraw() {
|
||||
drawLoading.value = true
|
||||
try {
|
||||
const openid = uni.getStorageSync('openid')
|
||||
const joinRes = await joinLottery({
|
||||
activity_id: Number(activityId.value),
|
||||
issue_id: Number(currentIssueId.value),
|
||||
channel: 'miniapp',
|
||||
count: 1,
|
||||
coupon_id: selectedCoupon.value?.id ? Number(selectedCoupon.value.id) : 0
|
||||
})
|
||||
|
||||
if (!joinRes) throw new Error('下单失败')
|
||||
const orderNo = joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no
|
||||
|
||||
// Simulate Wechat Pay flow (simplified)
|
||||
const payRes = await createWechatOrder({ openid, order_no: orderNo })
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
...payRes,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
// Get Result
|
||||
const res = await getLotteryResult(orderNo)
|
||||
const raw = res.list || res.items || res.data || res.result || (Array.isArray(res) ? res : [res])
|
||||
winItems.value = raw.map(i => ({
|
||||
title: i.title || i.name || '未知奖励',
|
||||
image: i.image || i.img || ''
|
||||
}))
|
||||
|
||||
showFlip.value = true
|
||||
setTimeout(() => {
|
||||
if(flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(winItems.value)
|
||||
}, 100)
|
||||
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '挑战失败', icon: 'none' })
|
||||
} finally {
|
||||
drawLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeIssues(list) {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map(i => ({
|
||||
id: i.id,
|
||||
title: i.title || i.name,
|
||||
no: i.no,
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeRewards(list) {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map(i => ({
|
||||
title: i.name || i.title,
|
||||
image: i.image || i.img || i.pic,
|
||||
percent: i.percent || 0
|
||||
}))
|
||||
}
|
||||
|
||||
async function fetchPropCards() { /* implementation same as other pages */ }
|
||||
async function fetchCoupons() { /* implementation same as other pages */ }
|
||||
|
||||
function showRules() {
|
||||
uni.showModal({ title: '规则', content: detail.value.rules || '暂无规则', showCancel: false })
|
||||
}
|
||||
|
||||
function closeFlip() { showFlip.value = false }
|
||||
|
||||
onLoad((opts) => {
|
||||
if (opts.id) {
|
||||
activityId.value = opts.id
|
||||
loadData(opts.id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
爬塔页面 - 沉浸式暗黑风格 (SCSS Integration)
|
||||
============================================ */
|
||||
|
||||
$local-gold: #FFD700; // 特殊金色,比全局更亮
|
||||
|
||||
.page-wrapper {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
background: $bg-dark;
|
||||
color: $text-dark-main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰 - 暗黑版 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10%; left: -20%;
|
||||
width: 600rpx; height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.1) 0%, transparent 70%);
|
||||
filter: blur(80rpx);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 10%; right: -10%;
|
||||
width: 500rpx; height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($local-gold, 0.08) 0%, transparent 70%);
|
||||
filter: blur(60rpx);
|
||||
border-radius: 50%;
|
||||
opacity: 0.5;
|
||||
animation: float 12s ease-in-out infinite reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(20rpx, 30rpx); }
|
||||
}
|
||||
|
||||
.bg-fixed {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
opacity: 0.3;
|
||||
z-index: 0;
|
||||
filter: blur(8rpx);
|
||||
}
|
||||
.bg-mask {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: linear-gradient(180deg, rgba($bg-dark, 0.85), $bg-dark 95%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $spacing-lg;
|
||||
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-xl;
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
.title-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main-title {
|
||||
font-size: 60rpx;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
display: block;
|
||||
text-shadow: 0 4rpx 16rpx rgba(0,0,0,0.6);
|
||||
background: linear-gradient(180deg, #fff, #b3b3b3);
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.sub-title {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.8;
|
||||
margin-top: $spacing-xs;
|
||||
display: block;
|
||||
letter-spacing: 4rpx;
|
||||
color: $brand-primary;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.rule-btn {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid $border-dark;
|
||||
padding: 12rpx 32rpx;
|
||||
border-radius: 100rpx;
|
||||
font-size: 24rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
transition: all 0.2s;
|
||||
color: rgba(255,255,255,0.9);
|
||||
|
||||
&:active {
|
||||
background: rgba(255,255,255,0.25);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
.tower-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.tower-level {
|
||||
width: 100%;
|
||||
background: $bg-dark-card;
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 48rpx;
|
||||
border-radius: $radius-xl;
|
||||
box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.3);
|
||||
margin-bottom: 40rpx;
|
||||
border: 1px solid $border-dark;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: zoomIn 0.5s ease-out backwards;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
}
|
||||
|
||||
&.current {
|
||||
background: rgba($local-gold, 0.15);
|
||||
border-color: rgba($local-gold, 0.5);
|
||||
box-shadow: 0 0 40rpx rgba($local-gold, 0.15), inset 0 0 20rpx rgba($local-gold, 0.05);
|
||||
}
|
||||
}
|
||||
.level-info { display: flex; flex-direction: column; z-index: 1; }
|
||||
.level-num {
|
||||
font-size: 24rpx;
|
||||
color: $text-dark-sub;
|
||||
margin-bottom: 8rpx;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.level-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: $text-dark-main;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.3);
|
||||
}
|
||||
.level-status {
|
||||
font-size: 24rpx;
|
||||
background: linear-gradient(135deg, $local-gold, $brand-secondary);
|
||||
color: #3e2723;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4rpx 16rpx rgba($brand-secondary, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rewards-preview {
|
||||
width: 100%;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
.rewards-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
.reward-item {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 160rpx;
|
||||
margin-right: 24rpx;
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
|
||||
@for $i from 1 through 5 {
|
||||
&:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.1}s;
|
||||
}
|
||||
}
|
||||
}
|
||||
.reward-img {
|
||||
width: 120rpx; height: 120rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255,255,255,0.05);
|
||||
margin-bottom: 16rpx;
|
||||
border: 1px solid $border-dark;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
.reward-name {
|
||||
font-size: 22rpx;
|
||||
color: $text-dark-sub;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.reward-prob {
|
||||
font-size: 20rpx;
|
||||
color: $local-gold;
|
||||
font-weight: 600;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.action-area {
|
||||
background: $bg-dark-card;
|
||||
backdrop-filter: blur(40rpx);
|
||||
padding: 24rpx 32rpx;
|
||||
border-radius: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1px solid $border-dark;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.5);
|
||||
margin-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
|
||||
animation: slideUp 0.6s ease-out backwards;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.price-display {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: $local-gold;
|
||||
font-weight: 700;
|
||||
margin-left: 20rpx;
|
||||
text-shadow: 0 0 20rpx rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
.currency { font-size: 28rpx; }
|
||||
.amount { font-size: 48rpx; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
|
||||
.unit { font-size: 24rpx; opacity: 0.8; font-weight: normal; }
|
||||
|
||||
.challenge-btn {
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
border-radius: 100rpx;
|
||||
padding: 0 60rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 0, 0.3);
|
||||
border: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: -100%; width: 100%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.flip-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999;
|
||||
}
|
||||
.flip-mask {
|
||||
position: absolute; top: 0; bottom: 0; width: 100%; background: rgba(0,0,0,0.85);
|
||||
backdrop-filter: blur(10rpx);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
.flip-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40rpx;
|
||||
justify-content: center;
|
||||
animation: zoomIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
.close-btn {
|
||||
margin-top: 60rpx;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 700;
|
||||
width: 50%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
align-self: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255,255,255,0.15);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
50%, 100% { left: 200%; }
|
||||
}
|
||||
</style>
|
||||
@ -1,25 +1,63 @@
|
||||
<template>
|
||||
<view class="bg-decoration"></view>
|
||||
<scroll-view class="page" scroll-y>
|
||||
<!-- 顶部 Banner -->
|
||||
<view class="banner" v-if="detail.banner">
|
||||
<image class="banner-img" :src="detail.banner" mode="widthFix" />
|
||||
</view>
|
||||
<view class="header">
|
||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||||
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选:¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
|
||||
</view>
|
||||
<view class="draw-actions">
|
||||
<button class="draw-btn" @click="() => openPayment(1)">参加一次</button>
|
||||
<button class="draw-btn" @click="() => openPayment(10)">参加十次</button>
|
||||
<button class="draw-btn secondary" @click="onMachineTry">试一试</button>
|
||||
</view>
|
||||
<view class="issues" v-if="showIssues && issues.length">
|
||||
<view class="issue-switch">
|
||||
<button class="switch-btn" @click="prevIssue">〈</button>
|
||||
<text class="issue-title">{{ currentIssueTitle }}</text>
|
||||
<button class="switch-btn" @click="nextIssue">〉</button>
|
||||
|
||||
<!-- 商品信息卡片 -->
|
||||
<view class="product-card">
|
||||
<view class="product-info">
|
||||
<image v-if="detail.banner" class="product-thumb" :src="detail.banner" mode="aspectFill" />
|
||||
<view class="product-detail">
|
||||
<view class="product-name">{{ detail.name || detail.title || '无限赏活动' }}</view>
|
||||
<view class="product-price">¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
|
||||
</view>
|
||||
<view class="product-actions">
|
||||
<view class="action-btn">📦 盒柜</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 期号切换条 -->
|
||||
<view class="issue-bar" v-if="showIssues && issues.length">
|
||||
<button class="nav-btn" @click="prevIssue">◀</button>
|
||||
<view class="issue-info">
|
||||
<text class="issue-label">{{ currentIssueTitle }}</text>
|
||||
</view>
|
||||
<button class="nav-btn" @click="nextIssue">▶</button>
|
||||
</view>
|
||||
|
||||
<!-- 玩法福利标签 -->
|
||||
<view class="gameplay-tags">
|
||||
<view class="tag tag-pool">聚宝盆</view>
|
||||
<view class="tag tag-drop">随机掉落 10%</view>
|
||||
<view class="tag tag-free">随机免单 10%</view>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部多档位抽赏按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="tier-btn" @click="() => openPayment(1)">
|
||||
<text class="tier-price">¥{{ (pricePerDrawYuan * 1).toFixed(2) }}</text>
|
||||
<text class="tier-label">抽1发</text>
|
||||
</button>
|
||||
<button class="tier-btn" @click="() => openPayment(3)">
|
||||
<text class="tier-price">¥{{ (pricePerDrawYuan * 3).toFixed(2) }}</text>
|
||||
<text class="tier-label">抽3发</text>
|
||||
</button>
|
||||
<button class="tier-btn" @click="() => openPayment(5)">
|
||||
<text class="tier-price">¥{{ (pricePerDrawYuan * 5).toFixed(2) }}</text>
|
||||
<text class="tier-label">抽5发</text>
|
||||
</button>
|
||||
<button class="tier-btn tier-hot" @click="() => openPayment(10)">
|
||||
<text class="tier-price">¥{{ (pricePerDrawYuan * 10).toFixed(2) }}</text>
|
||||
<text class="tier-label">抽10发</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||||
<view class="flip-mask" @tap="closeFlip"></view>
|
||||
<view class="flip-content" @tap.stop>
|
||||
@ -38,8 +76,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import ElCard from '../../../components/ElCard.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import FlipGrid from '../../../components/FlipGrid.vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import PaymentPopup from '../../../components/PaymentPopup.vue'
|
||||
@ -218,19 +255,6 @@ function nextIssue() {
|
||||
currentIssueId.value = (cur && cur.id) || ''
|
||||
}
|
||||
|
||||
async function ensureElCard() {
|
||||
const inst = getCurrentInstance()
|
||||
const app = inst && inst.appContext && inst.appContext.app
|
||||
let comp = null
|
||||
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
|
||||
comp = window.ElementPlus.ElCard
|
||||
} else {
|
||||
comp = ElCard
|
||||
}
|
||||
if (app && !app.component('el-card')) app.component('el-card', comp)
|
||||
}
|
||||
|
||||
|
||||
function onPreviewBanner() {
|
||||
const url = detail.value.banner || ''
|
||||
if (url) uni.previewImage({ urls: [url], current: url })
|
||||
@ -381,49 +405,358 @@ onLoad((opts) => {
|
||||
fetchDetail(id)
|
||||
fetchIssues(id)
|
||||
}
|
||||
ensureElCard()
|
||||
})
|
||||
|
||||
function closeFlip() { showFlip.value = false }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { height: 100vh; padding-bottom: 140rpx }
|
||||
.banner { padding: 24rpx }
|
||||
.banner-img { width: 100% }
|
||||
.header { padding: 0 24rpx }
|
||||
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
|
||||
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||||
.btn { flex: 1 }
|
||||
.primary { background-color: #007AFF; color: #fff }
|
||||
.draw-actions { display: flex; gap: 12rpx; padding: 24rpx }
|
||||
.draw-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
|
||||
.draw-btn.secondary { background: #ffd166; color: #6b4b1f }
|
||||
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000 }
|
||||
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1 }
|
||||
.flip-content { position: relative; display: flex; flex-direction: column; height: 100%; padding: 24rpx; z-index: 2 }
|
||||
.overlay-close { background: #ffd166; color: #6b4b1f; border-radius: 999rpx; align-self: flex-end }
|
||||
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||||
.issues-list { }
|
||||
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
|
||||
.switch-btn { width: 72rpx; height: 72rpx; border-radius: 999rpx; background: #fff3df; border: 2rpx solid #f0c58a; color: #8a5a2b }
|
||||
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
|
||||
.rewards { width: 100%; margin-top: 24rpx }
|
||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||||
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||||
.el-reward-card { margin-bottom: 12rpx }
|
||||
.el-card-header { display: flex; align-items: center; justify-content: space-between }
|
||||
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||||
.card-image-wrap { position: relative; padding-bottom: 48rpx }
|
||||
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
|
||||
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
|
||||
.prob-corner.tl { top: 12rpx; left: 12rpx }
|
||||
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
|
||||
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||||
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||
.issues-empty { font-size: 24rpx; color: #999 }
|
||||
<style lang="scss" scoped>
|
||||
/* 奇盒潮玩 - 无限赏活动页面 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
background: transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: $bg-page;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&::before {
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.12), transparent 70%);
|
||||
top: -200rpx;
|
||||
left: -200rpx;
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 500rpx;
|
||||
height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($accent-gold, 0.15), transparent 70%);
|
||||
bottom: 10%;
|
||||
right: -100rpx;
|
||||
animation: float 12s ease-in-out infinite reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(30rpx, 50rpx); }
|
||||
}
|
||||
|
||||
.banner {
|
||||
padding: $spacing-lg $spacing-lg 0;
|
||||
animation: fadeInDown 0.6s $ease-out;
|
||||
}
|
||||
.banner-img {
|
||||
width: 100%;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
|
||||
/* 商品信息卡片 */
|
||||
.product-card {
|
||||
margin: $spacing-lg;
|
||||
background: $bg-glass;
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
animation: fadeInUp 0.6s $ease-out 0.1s backwards;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.product-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
.product-thumb {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: $radius-md;
|
||||
flex-shrink: 0;
|
||||
background: $bg-page;
|
||||
box-shadow: $shadow-inner;
|
||||
}
|
||||
.product-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.product-name {
|
||||
font-size: $font-lg;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: $spacing-sm;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.product-price {
|
||||
font-size: $font-xl;
|
||||
font-weight: 800;
|
||||
color: $brand-primary;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
}
|
||||
.product-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
.action-btn {
|
||||
background: rgba($brand-primary, 0.05);
|
||||
border: 1rpx solid rgba($brand-primary, 0.2);
|
||||
border-radius: $radius-sm;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
font-size: $font-sm;
|
||||
color: $brand-primary-dark;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
transition: all $transition-fast;
|
||||
}
|
||||
.action-btn:active {
|
||||
background: rgba($brand-primary, 0.1);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 期号切换条 */
|
||||
.issue-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-lg;
|
||||
margin: 0 $spacing-lg $spacing-lg;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
background: $bg-glass;
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: $radius-round;
|
||||
box-shadow: $shadow-sm;
|
||||
animation: fadeInUp 0.6s $ease-out 0.2s backwards;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.nav-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-page;
|
||||
color: $text-sub;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-sm;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
transition: all $transition-fast;
|
||||
border: none;
|
||||
|
||||
&:active {
|
||||
background: darken($bg-page, 5%);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
.issue-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
.issue-label {
|
||||
font-size: $font-lg;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
/* 玩法福利标签 */
|
||||
.gameplay-tags {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
padding: 0 $spacing-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
flex-wrap: wrap;
|
||||
animation: fadeInUp 0.6s $ease-out 0.3s backwards;
|
||||
}
|
||||
.tag {
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
border-radius: $radius-round;
|
||||
font-size: $font-sm;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: $shadow-sm;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.tag-pool {
|
||||
background: $color-success;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba($color-success, 0.3);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.tag-drop {
|
||||
background: $gradient-brand;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.3);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.tag-free {
|
||||
background: $gradient-gold;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba($accent-gold, 0.3);
|
||||
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.1);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 底部多档位抽赏按钮 */
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-lg $spacing-lg;
|
||||
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20rpx);
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
z-index: 999;
|
||||
animation: slideUp $transition-slow $ease-out backwards;
|
||||
border-top: 1rpx solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.tier-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-md $spacing-xs;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid $border-color-light;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: $bg-page;
|
||||
}
|
||||
}
|
||||
.tier-price {
|
||||
font-size: $font-lg;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
}
|
||||
.tier-label {
|
||||
font-size: $font-xs;
|
||||
color: $text-sub;
|
||||
margin-top: 4rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tier-hot {
|
||||
background: $gradient-brand;
|
||||
border: none;
|
||||
box-shadow: $shadow-warm;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.tier-price, .tier-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: 'HOT';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, $accent-red, #D32F2F);
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
font-weight: 800;
|
||||
padding: 4rpx 10rpx;
|
||||
border-bottom-left-radius: $radius-md;
|
||||
box-shadow: -2rpx 2rpx 4rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
.tier-hot .tier-price, .tier-hot .tier-label {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 翻牌弹窗 */
|
||||
.flip-overlay {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
.flip-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1;
|
||||
}
|
||||
.flip-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24rpx;
|
||||
z-index: 2;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
animation: zoomIn 0.3s $ease-bounce;
|
||||
}
|
||||
.overlay-close {
|
||||
margin-top: 60rpx;
|
||||
width: 240rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: rgba(255,255,255,0.15) !important;
|
||||
border: 1rpx solid rgba(255,255,255,0.3);
|
||||
color: #FFFFFF !important;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 600;
|
||||
font-size: 30rpx;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:active {
|
||||
background: rgba(255,255,255,0.25) !important;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,32 +1,99 @@
|
||||
<template>
|
||||
<scroll-view class="page" scroll-y>
|
||||
<view class="banner" v-if="detail.banner">
|
||||
<image class="banner-img" :src="detail.banner" mode="widthFix" />
|
||||
</view>
|
||||
<view class="header">
|
||||
<view class="title">{{ detail.name || detail.title || '-' }}</view>
|
||||
<view class="meta" v-if="detail.price_draw !== undefined">抽选价:¥{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
|
||||
</view>
|
||||
|
||||
<view class="issues" v-if="showIssues && issues.length">
|
||||
<view class="issue-switch">
|
||||
<button class="switch-btn" @click="prevIssue">〈</button>
|
||||
<text class="issue-title">{{ currentIssueTitle }}</text>
|
||||
<button class="switch-btn" @click="nextIssue">〉</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 引入位置选择组件 -->
|
||||
<view class="selector-section" v-if="activityId && currentIssueId">
|
||||
<YifanSelector
|
||||
:activity-id="activityId"
|
||||
:issue-id="currentIssueId"
|
||||
:price-per-draw="Number(detail.price_draw || 0) / 100"
|
||||
@payment-success="onPaymentSuccess"
|
||||
/>
|
||||
<view class="page-wrapper">
|
||||
<!-- 背景装饰 -->
|
||||
<view class="bg-decoration">
|
||||
<view class="orb orb-1"></view>
|
||||
<view class="orb orb-2"></view>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
<!-- 顶部背景图(模糊处理) -->
|
||||
<view class="page-bg">
|
||||
<image class="bg-image" :src="detail.banner" mode="aspectFill" />
|
||||
<view class="bg-mask"></view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位(如果有自定义导航栏需求) -->
|
||||
<!-- <view class="nav-bar-placeholder"></view> -->
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<scroll-view class="main-scroll" scroll-y>
|
||||
|
||||
<!-- 头部信息卡片 -->
|
||||
<view class="header-card animate-enter">
|
||||
<image class="header-cover" :src="detail.banner" mode="aspectFill" />
|
||||
<view class="header-info">
|
||||
<view class="header-title">{{ detail.name || detail.title || '一番赏活动' }}</view>
|
||||
<view class="header-price-row">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-num">{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</text>
|
||||
<text class="price-unit">/发</text>
|
||||
</view>
|
||||
<view class="header-tags">
|
||||
<view class="tag-item">超高爆率</view>
|
||||
<view class="tag-item">公平公正</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="action-btn" @tap="showRules">
|
||||
<text class="icon">📋</text>
|
||||
<text>规则</text>
|
||||
</view>
|
||||
<view class="action-btn" @tap="goCabinet">
|
||||
<text class="icon">📦</text>
|
||||
<text>盒柜</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 赏品概览 -->
|
||||
<view class="section-container animate-enter stagger-1" v-if="currentIssueRewards.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">赏品一览</text>
|
||||
<text class="section-more">查看全部 ></text>
|
||||
</view>
|
||||
<scroll-view class="preview-scroll" scroll-x>
|
||||
<view class="preview-item" v-for="(item, idx) in currentIssueRewards" :key="idx">
|
||||
<view class="prize-tag" :class="{ 'tag-boss': item.boss }">{{ item.boss ? 'BOSS' : (item.grade || '赏') }}</view>
|
||||
<image class="preview-img" :src="item.image" mode="aspectFill" />
|
||||
<view class="preview-name">{{ item.title }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 选号区域 -->
|
||||
<view class="section-container selector-container animate-enter stagger-2">
|
||||
<!-- 期号切换 -->
|
||||
<view class="issue-header">
|
||||
<view class="issue-switch-btn" @click="prevIssue">
|
||||
<text class="arrow">◀</text>
|
||||
</view>
|
||||
<view class="issue-info-center">
|
||||
<text class="issue-current-text">{{ currentIssueTitle }}</text>
|
||||
<text class="issue-status-badge">进行中</text>
|
||||
</view>
|
||||
<view class="issue-switch-btn" @click="nextIssue">
|
||||
<text class="arrow">▶</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选号组件 -->
|
||||
<view class="selector-body" v-if="activityId && currentIssueId">
|
||||
<YifanSelector
|
||||
:activity-id="activityId"
|
||||
:issue-id="currentIssueId"
|
||||
:price-per-draw="Number(detail.price_draw || 0) / 100"
|
||||
@payment-success="onPaymentSuccess"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部垫高 -->
|
||||
<view style="height: 180rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 翻牌弹窗 -->
|
||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||||
<view class="flip-mask" @tap="closeFlip"></view>
|
||||
<view class="flip-content" @tap.stop>
|
||||
@ -38,8 +105,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
import ElCard from '../../../components/ElCard.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import FlipGrid from '../../../components/FlipGrid.vue'
|
||||
import YifanSelector from '@/components/YifanSelector.vue'
|
||||
@ -65,6 +131,26 @@ const currentIssueTitle = computed(() => {
|
||||
const t = (cur && (cur.title || ('第' + (cur.no || '-') + '期'))) || '-'
|
||||
return t
|
||||
})
|
||||
// 当前期剩余数量
|
||||
const currentIssueRemain = computed(() => {
|
||||
const arr = issues.value || []
|
||||
const cur = arr[selectedIssueIndex.value]
|
||||
return cur && cur.remain !== undefined ? cur.remain : ''
|
||||
})
|
||||
|
||||
// 显示规则
|
||||
function showRules() {
|
||||
uni.showModal({
|
||||
title: '活动规则',
|
||||
content: detail.value.rules || '1. 选择号码进行抽选\n2. 每个号码对应一个奖品\n3. 已售号码不可再选',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转盒柜
|
||||
function goCabinet() {
|
||||
uni.navigateTo({ url: '/pages/cabinet/index' })
|
||||
}
|
||||
|
||||
function statusToText(s) {
|
||||
if (s === 1) return '进行中'
|
||||
@ -232,17 +318,7 @@ function nextIssue() {
|
||||
currentIssueId.value = (cur && cur.id) || ''
|
||||
}
|
||||
|
||||
async function ensureElCard() {
|
||||
const inst = getCurrentInstance()
|
||||
const app = inst && inst.appContext && inst.appContext.app
|
||||
let comp = null
|
||||
if (typeof window !== 'undefined' && window.ElementPlus && window.ElementPlus.ElCard) {
|
||||
comp = window.ElementPlus.ElCard
|
||||
} else {
|
||||
comp = ElCard
|
||||
}
|
||||
if (app && !app.component('el-card')) app.component('el-card', comp)
|
||||
}
|
||||
|
||||
|
||||
function onPreviewBanner() {
|
||||
const url = detail.value.banner || ''
|
||||
@ -292,61 +368,436 @@ onLoad((opts) => {
|
||||
fetchIssues(id)
|
||||
fetchWinRecords(id)
|
||||
}
|
||||
ensureElCard()
|
||||
})
|
||||
|
||||
function closeFlip() { showFlip.value = false }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { height: 100vh; padding-bottom: 140rpx }
|
||||
.banner { padding: 24rpx }
|
||||
.banner-img { width: 100% }
|
||||
.header { padding: 0 24rpx }
|
||||
.title { font-size: 36rpx; font-weight: 700; color: #DD2C00; text-align: center }
|
||||
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
|
||||
.actions { display: flex; padding: 24rpx; gap: 16rpx }
|
||||
.btn { flex: 1 }
|
||||
.primary { background-color: #007AFF; color: #fff }
|
||||
.draw-actions { display: flex; gap: 12rpx; padding: 24rpx }
|
||||
.draw-btn { flex: 1; background: #007AFF; color: #fff; border-radius: 8rpx }
|
||||
.draw-btn.secondary { background: #ffd166; color: #6b4b1f }
|
||||
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000 }
|
||||
.flip-mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1 }
|
||||
.flip-content { position: relative; display: flex; flex-direction: column; height: 100%; padding: 24rpx; z-index: 2 }
|
||||
.overlay-close { background: #ffd166; color: #6b4b1f; border-radius: 999rpx; align-self: flex-end }
|
||||
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
|
||||
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
|
||||
.issues-list { }
|
||||
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
|
||||
.switch-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #fff3df;
|
||||
border: 2rpx solid #f0c58a;
|
||||
color: #8a5a2b;
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
一番赏页面 - 高级设计重构 (SCSS Integration)
|
||||
============================================ */
|
||||
|
||||
.page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.2), transparent 70%);
|
||||
top: -200rpx;
|
||||
left: -200rpx;
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
width: 500rpx;
|
||||
height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($accent-gold, 0.2), transparent 70%);
|
||||
bottom: 20%;
|
||||
right: -100rpx;
|
||||
animation: float 12s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(30rpx, 50rpx); }
|
||||
}
|
||||
|
||||
/* 顶部背景 */
|
||||
.page-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 700rpx; /* 加高背景区域 */
|
||||
z-index: 1;
|
||||
}
|
||||
.bg-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(30rpx) brightness(0.9); /* 降低亮度提升文字对比度 */
|
||||
transform: scale(1.1); /* 防止模糊边缘 */
|
||||
}
|
||||
.bg-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba($bg-page, 0.2) 0%, $bg-page 90%, $bg-page 100%);
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 头部卡片 */
|
||||
.header-card {
|
||||
margin: $spacing-xl $spacing-lg;
|
||||
background: rgba($bg-card, 0.85);
|
||||
backdrop-filter: blur(24rpx);
|
||||
border-radius: $radius-xl;
|
||||
padding: $spacing-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: $shadow-card;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* 光泽效果 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
|
||||
}
|
||||
}
|
||||
.header-cover {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: $radius-md;
|
||||
margin-right: $spacing-lg;
|
||||
background: $bg-secondary;
|
||||
box-shadow: $shadow-md;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 180rpx;
|
||||
}
|
||||
.header-title {
|
||||
font-size: $font-xl;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
margin-bottom: $spacing-xs;
|
||||
line-height: 1.3;
|
||||
@include text-ellipsis(2);
|
||||
}
|
||||
.header-price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: $brand-primary;
|
||||
margin-bottom: $spacing-sm;
|
||||
text-shadow: 0 2rpx 4rpx rgba($brand-primary, 0.1);
|
||||
}
|
||||
.price-symbol { font-size: $font-md; font-weight: 700; }
|
||||
.price-num { font-size: $font-xxl; font-weight: 900; margin: 0 4rpx; font-family: 'DIN Alternate', sans-serif; }
|
||||
.price-unit { font-size: $font-sm; color: $text-sub; margin-left: 4rpx; }
|
||||
|
||||
.header-tags {
|
||||
display: flex;
|
||||
gap: $spacing-xs;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tag-item {
|
||||
font-size: $font-xs;
|
||||
color: $brand-primary-dark;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
font-weight: 600;
|
||||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-lg;
|
||||
margin-left: 20rpx;
|
||||
padding-left: $spacing-lg;
|
||||
border-left: 1rpx solid rgba(0,0,0,0.06);
|
||||
justify-content: center;
|
||||
height: 140rpx;
|
||||
}
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: $font-xs;
|
||||
color: $text-sub;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
color: $text-main;
|
||||
}
|
||||
}
|
||||
.action-btn .icon {
|
||||
font-size: $font-xl;
|
||||
margin-bottom: 6rpx;
|
||||
filter: grayscale(0.2);
|
||||
}
|
||||
|
||||
/* 通用板块容器 */
|
||||
.section-container {
|
||||
margin: 0 $spacing-lg $spacing-lg;
|
||||
background: rgba(255, 255, 255, 0.9); /* 略微透明 */
|
||||
border-radius: $radius-xl;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
/* 板块标题 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: $font-lg;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
position: relative;
|
||||
padding-left: $spacing-lg;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8rpx;
|
||||
height: 28rpx;
|
||||
background: $gradient-brand;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
}
|
||||
.section-more {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::after {
|
||||
content: '>';
|
||||
font-family: monospace;
|
||||
margin-left: 6rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
/* 奖品概览 */
|
||||
.preview-scroll {
|
||||
white-space: nowrap;
|
||||
margin: 0 -$spacing-lg; /* 负边距抵消padding */
|
||||
padding: 0 $spacing-lg;
|
||||
width: calc(100% + 40rpx);
|
||||
}
|
||||
.preview-item {
|
||||
display: inline-block;
|
||||
width: 200rpx;
|
||||
margin-right: $spacing-lg;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
.preview-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: $radius-lg;
|
||||
background: $bg-secondary;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: $shadow-sm;
|
||||
border: 1rpx solid rgba(0,0,0,0.03);
|
||||
}
|
||||
.preview-name {
|
||||
font-size: $font-sm;
|
||||
color: $text-secondary;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
.prize-tag {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
left: 10rpx;
|
||||
background: rgba(0,0,0,0.6);
|
||||
color: #fff;
|
||||
font-size: $font-xs;
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
z-index: 10;
|
||||
font-weight: 700;
|
||||
backdrop-filter: blur(4rpx);
|
||||
transform: scale(0.9);
|
||||
transform-origin: top left;
|
||||
}
|
||||
.prize-tag.tag-boss {
|
||||
background: $gradient-brand;
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.4);
|
||||
}
|
||||
|
||||
/* 选号区容器 */
|
||||
.selector-container {
|
||||
min-height: 800rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba($bg-card, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
/* 期号头部 */
|
||||
.issue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
background: $bg-grey;
|
||||
border-radius: $radius-round; /* 胶囊形 */
|
||||
padding: 10rpx;
|
||||
border: 1rpx solid $border-color-light;
|
||||
}
|
||||
.issue-switch-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
background: $bg-card;
|
||||
border-radius: 50%;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: all 0.2s;
|
||||
color: $text-secondary;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background: $bg-secondary;
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
.arrow {
|
||||
font-size: $font-sm;
|
||||
font-weight: 800;
|
||||
}
|
||||
.issue-info-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
.issue-current-text {
|
||||
font-size: $font-lg;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
}
|
||||
.issue-status-badge {
|
||||
font-size: $font-xs;
|
||||
color: $uni-color-success;
|
||||
background: rgba($uni-color-success, 0.1);
|
||||
padding: 2rpx $spacing-md;
|
||||
border-radius: $radius-round;
|
||||
margin-top: 4rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.selector-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 翻牌弹窗 */
|
||||
.flip-overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 10000; }
|
||||
.flip-mask {
|
||||
position: absolute; left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.75);
|
||||
backdrop-filter: blur(10rpx);
|
||||
z-index: 1;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
.flip-content {
|
||||
position: relative; display: flex; flex-direction: column; height: 100%; padding: 40rpx; z-index: 2;
|
||||
animation: scaleIn 0.3s ease-out;
|
||||
}
|
||||
.overlay-close {
|
||||
background: rgba(255,255,255,0.2) !important;
|
||||
color: #FFFFFF !important;
|
||||
border-radius: 999rpx;
|
||||
align-self: center;
|
||||
margin-top: 40rpx;
|
||||
font-weight: 600;
|
||||
border: 1rpx solid rgba(255,255,255,0.3);
|
||||
padding: 10rpx 60rpx;
|
||||
font-size: 30rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
|
||||
&:active {
|
||||
background: rgba(255,255,255,0.3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
.animate-enter {
|
||||
animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
|
||||
}
|
||||
|
||||
.stagger-1 { animation-delay: 0.1s; }
|
||||
.stagger-2 { animation-delay: 0.2s; }
|
||||
.stagger-3 { animation-delay: 0.3s; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(40rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
|
||||
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
|
||||
.reward-card { background: #fff; border-radius: 12rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); margin-bottom: 12rpx }
|
||||
.el-reward-card { margin-bottom: 12rpx }
|
||||
.el-card-header { display: flex; align-items: center; justify-content: space-between }
|
||||
.el-card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||||
.card-image-wrap { position: relative; padding-bottom: 48rpx }
|
||||
.card-image { width: 100%; height: auto; display: block; background: #f0f4ff; position: relative; z-index: 1 }
|
||||
.prob-corner { position: absolute; background: rgba(221,82,77,0.9); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; z-index: 2 }
|
||||
.prob-corner.tl { top: 12rpx; left: 12rpx }
|
||||
.card-body { display: flex; align-items: center; justify-content: space-between; padding: 12rpx }
|
||||
.card-title { font-size: 28rpx; color: #222; flex: 1; margin-right: 8rpx; word-break: break-all }
|
||||
.badge-boss { background: #ff9f0a; color: #222; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.badge-count { background: #ffd166; color: #6b4b1f; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx }
|
||||
.rewards-empty { font-size: 24rpx; color: #999 }
|
||||
.issues-empty { font-size: 24rpx; color: #999 }
|
||||
</style>
|
||||
|
||||
@ -148,11 +148,87 @@ onLoad((opts) => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap { padding: 24rpx }
|
||||
.form-item { display: flex; align-items: center; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 12rpx }
|
||||
.label { width: 160rpx; font-size: 28rpx; color: #666 }
|
||||
.input { flex: 1; font-size: 28rpx }
|
||||
.submit { width: 100%; margin-top: 20rpx }
|
||||
.error { color: #e43; margin-top: 12rpx }
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 地址编辑页面
|
||||
采用暖橙色调的表单设计
|
||||
============================================ */
|
||||
|
||||
.wrap {
|
||||
padding: $spacing-lg;
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
}
|
||||
|
||||
/* 表单项 */
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFFFFF;
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-lg $spacing-xl;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: $shadow-md;
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 160rpx;
|
||||
font-size: $font-md;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
font-size: $font-md;
|
||||
color: $text-main;
|
||||
background: transparent;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
margin-top: 60rpx;
|
||||
background: $gradient-brand !important;
|
||||
color: #FFFFFF !important;
|
||||
border-radius: $radius-round;
|
||||
font-size: $font-lg;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
box-shadow: $shadow-warm;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
box-shadow: none;
|
||||
background: $text-disabled !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error {
|
||||
color: $color-error;
|
||||
font-size: $font-sm;
|
||||
margin-top: $spacing-lg;
|
||||
padding: $spacing-md;
|
||||
background: rgba($color-error, 0.1);
|
||||
border-radius: $radius-md;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@ -113,18 +113,158 @@ onLoad(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap { padding: 24rpx }
|
||||
.header { display: flex; justify-content: flex-end; margin-bottom: 12rpx }
|
||||
.add { font-size: 28rpx }
|
||||
.addr { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
|
||||
.addr-row { display: flex; align-items: center; margin-bottom: 8rpx }
|
||||
.name { font-size: 30rpx; margin-right: 12rpx }
|
||||
.phone { font-size: 28rpx; color: #666 }
|
||||
.default { font-size: 24rpx; color: #007AFF; margin-left: 10rpx }
|
||||
.region { font-size: 26rpx; color: #666 }
|
||||
.detail { font-size: 26rpx; color: #333 }
|
||||
.addr-actions { display: flex; justify-content: flex-end; gap: 12rpx; margin-top: 12rpx }
|
||||
.empty { text-align: center; color: #999; margin-top: 40rpx }
|
||||
.error { color: #e43; margin-bottom: 12rpx }
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 地址管理页面
|
||||
采用暖橙色调的卡片列表设计
|
||||
============================================ */
|
||||
|
||||
.wrap {
|
||||
padding: $spacing-md;
|
||||
min-height: 100vh;
|
||||
background-color: $bg-page;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
.add {
|
||||
font-size: $font-md;
|
||||
background: $gradient-brand !important;
|
||||
color: #FFFFFF !important;
|
||||
border-radius: $radius-round;
|
||||
padding: 0 $spacing-xl;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: $shadow-warm;
|
||||
}
|
||||
.add:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
/* 地址卡片 */
|
||||
.addr {
|
||||
background: #FFFFFF;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-lg;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: $shadow-sm;
|
||||
animation: fadeInUp 0.4s ease-out backwards;
|
||||
}
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
.addr:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-main {
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.addr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
.addr-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: $font-lg;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
}
|
||||
.phone {
|
||||
font-size: $font-md;
|
||||
color: $text-sub;
|
||||
}
|
||||
.default {
|
||||
font-size: $font-xs;
|
||||
color: #FFFFFF;
|
||||
background: $gradient-brand;
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 500;
|
||||
}
|
||||
.region {
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
.detail {
|
||||
font-size: $font-md;
|
||||
color: $text-main;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.addr-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $spacing-md;
|
||||
margin-top: $spacing-lg;
|
||||
padding-top: $spacing-lg;
|
||||
border-top: 1rpx solid $border-color-light;
|
||||
}
|
||||
.addr-actions button {
|
||||
font-size: $font-sm;
|
||||
height: 52rpx;
|
||||
line-height: 52rpx;
|
||||
padding: 0 $spacing-lg;
|
||||
border-radius: $radius-round;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
background: $bg-secondary;
|
||||
color: $text-main;
|
||||
|
||||
&::after { border: none; }
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
background: darken($bg-secondary, 5%);
|
||||
}
|
||||
}
|
||||
.addr-actions button[type="warn"] {
|
||||
background: rgba($color-error, 0.1) !important;
|
||||
color: $color-error !important;
|
||||
}
|
||||
.addr-actions button:not([type]) {
|
||||
background: $bg-secondary !important;
|
||||
color: $text-main !important;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: $text-sub;
|
||||
margin-top: 120rpx;
|
||||
font-size: $font-md;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error {
|
||||
color: $color-error;
|
||||
font-size: $font-sm;
|
||||
margin-bottom: $spacing-md;
|
||||
padding: $spacing-md;
|
||||
background: rgba($color-error, 0.1);
|
||||
border-radius: $radius-md;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -62,20 +62,65 @@
|
||||
<!-- Tab 1: 已申请发货 -->
|
||||
<block v-if="currentTab === 1">
|
||||
<view v-if="loading && shippedList.length === 0" class="status-text">加载中...</view>
|
||||
<view v-else-if="!shippedList || shippedList.length === 0" class="status-text">暂无已发货记录</view>
|
||||
<view v-else-if="!shippedList || shippedList.length === 0" class="status-text">暂无发货记录</view>
|
||||
|
||||
<view v-else class="inventory-grid">
|
||||
<view v-for="(item, index) in shippedList" :key="index" class="inventory-item">
|
||||
<!-- 已发货仅展示 -->
|
||||
<image :src="item.image" mode="aspectFill" class="item-image" @error="onImageError(index, 'shipped')" />
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.name || '未命名道具' }}</text>
|
||||
<text class="item-count">x{{ item.count || 1 }}</text>
|
||||
<text class="item-status">已申请发货</text>
|
||||
<text class="item-meta" v-if="item.express_code || item.express_no">快递:{{ item.express_code }} {{ item.express_no }}</text>
|
||||
<text class="item-meta" v-if="item.shipped_at">发货时间:{{ formatDate(item.shipped_at) }}</text>
|
||||
<text class="item-meta" v-if="item.received_at">签收时间:{{ formatDate(item.received_at) }}</text>
|
||||
</view>
|
||||
<view v-else class="shipment-list">
|
||||
<view v-for="(item, index) in shippedList" :key="index" class="shipment-card">
|
||||
<!-- 头部:批次信息和状态 -->
|
||||
<view class="shipment-header">
|
||||
<view class="shipment-batch">
|
||||
<text class="batch-label">发货单</text>
|
||||
<text class="batch-no" v-if="item.batch_no">{{ item.batch_no }}</text>
|
||||
<view class="count-badge">{{ item.count }}件商品</view>
|
||||
</view>
|
||||
<view class="shipment-status" :class="getStatusClass(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品缩略图列表 -->
|
||||
<view class="product-thumbnails">
|
||||
<view class="thumb-scroll">
|
||||
<image
|
||||
v-for="(img, imgIdx) in item.product_images.slice(0, 4)"
|
||||
:key="imgIdx"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
class="thumb-img"
|
||||
@error="onThumbError(index, imgIdx)"
|
||||
/>
|
||||
<view class="thumb-more" v-if="item.product_images.length > 4">
|
||||
+{{ item.product_images.length - 4 }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 商品名称列表 -->
|
||||
<view class="product-names">
|
||||
<text class="product-name-item" v-for="(name, nIdx) in item.product_names.slice(0, 3)" :key="nIdx">
|
||||
{{ name }}
|
||||
</text>
|
||||
<text class="product-name-more" v-if="item.product_names.length > 3">
|
||||
等{{ item.product_names.length }}件商品
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 物流信息 -->
|
||||
<view class="shipment-express" v-if="item.express_code || item.express_no">
|
||||
<view class="express-icon">
|
||||
<text class="iconfont">📦</text>
|
||||
</view>
|
||||
<view class="express-info">
|
||||
<text class="express-company">{{ item.express_code || '待发货' }}</text>
|
||||
<text class="express-no" v-if="item.express_no">{{ item.express_no }}</text>
|
||||
</view>
|
||||
<text class="express-copy" v-if="item.express_no" @tap="copyExpressNo(item.express_no)">复制</text>
|
||||
</view>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<view class="shipment-time">
|
||||
<text class="time-item" v-if="item.created_at">申请时间:{{ formatDate(item.created_at) }}</text>
|
||||
<text class="time-item" v-if="item.shipped_at">发货时间:{{ formatDate(item.shipped_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading && shippedList.length > 0" class="loading-more">加载更多...</view>
|
||||
@ -217,6 +262,42 @@ function formatDate(dateStr) {
|
||||
return `${y}-${m}-${da} ${h}:${min}`
|
||||
}
|
||||
|
||||
// 发货状态辅助函数
|
||||
function getStatusClass(status) {
|
||||
const statusMap = {
|
||||
1: 'status-pending', // 待发货
|
||||
2: 'status-shipped', // 已发货
|
||||
3: 'status-delivered', // 已签收
|
||||
4: 'status-cancelled' // 已取消
|
||||
}
|
||||
return statusMap[status] || 'status-pending'
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
1: '待发货',
|
||||
2: '运输中',
|
||||
3: '已签收',
|
||||
4: '已取消'
|
||||
}
|
||||
return statusMap[status] || '待发货'
|
||||
}
|
||||
|
||||
function copyExpressNo(expressNo) {
|
||||
uni.setClipboardData({
|
||||
data: expressNo,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已复制单号', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onThumbError(shipmentIndex, imgIndex) {
|
||||
if (shippedList.value[shipmentIndex] && shippedList.value[shipmentIndex].product_images) {
|
||||
shippedList.value[shipmentIndex].product_images[imgIndex] = '/static/logo.png'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadShipments(uid) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
@ -227,21 +308,51 @@ async function loadShipments(uid) {
|
||||
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
|
||||
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
|
||||
else if (Array.isArray(res)) { list = res; total = res.length }
|
||||
const mapped = list.map(s => ({
|
||||
image: '/static/logo.png',
|
||||
name: '发货单',
|
||||
count: s.count ?? (Array.isArray(s.inventory_ids) ? s.inventory_ids.length : 0),
|
||||
express_code: s.express_code || '',
|
||||
express_no: s.express_no || '',
|
||||
shipped_at: s.shipped_at || '',
|
||||
received_at: s.received_at || '',
|
||||
status: s.status
|
||||
}))
|
||||
|
||||
// 处理每个发货单 - 直接使用返回的 products 数组
|
||||
const mapped = list.map(s => {
|
||||
const products = s.products || []
|
||||
let productImages = []
|
||||
let productNames = []
|
||||
|
||||
// 从 products 数组中提取图片和名称
|
||||
products.forEach(product => {
|
||||
if (product) {
|
||||
// image 字段是 JSON 数组字符串,需要解析
|
||||
const rawImg = product.image || product.images || product.main_image
|
||||
const img = cleanUrl(rawImg)
|
||||
productImages.push(img)
|
||||
productNames.push(product.name || product.title || '商品')
|
||||
}
|
||||
})
|
||||
|
||||
// 如果没有获取到任何图片,使用默认图片
|
||||
if (productImages.length === 0) {
|
||||
productImages = ['/static/logo.png']
|
||||
productNames = ['未知商品']
|
||||
}
|
||||
|
||||
return {
|
||||
batch_no: s.batch_no || '',
|
||||
count: s.count ?? (Array.isArray(s.inventory_ids) ? s.inventory_ids.length : 0),
|
||||
product_ids: s.product_ids || [],
|
||||
product_images: productImages,
|
||||
product_names: productNames,
|
||||
express_code: s.express_code || '',
|
||||
express_no: s.express_no || '',
|
||||
created_at: s.created_at || '',
|
||||
shipped_at: s.shipped_at || '',
|
||||
received_at: s.received_at || '',
|
||||
status: s.status || 1
|
||||
}
|
||||
})
|
||||
|
||||
const next = page.value === 1 ? mapped : [...shippedList.value, ...mapped]
|
||||
shippedList.value = next
|
||||
if (list.length < pageSize.value || (page.value * pageSize.value >= total && total > 0)) { hasMore.value = false } else { page.value += 1 }
|
||||
if (list.length === 0) { hasMore.value = false }
|
||||
} catch (e) {
|
||||
console.error('Load shipments error:', e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
@ -569,246 +680,480 @@ async function onShip() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-status {
|
||||
font-size: 24rpx;
|
||||
color: #007AFF;
|
||||
margin-top: 4rpx;
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 货柜页面
|
||||
采用现代卡片式布局,统一设计语言
|
||||
============================================ */
|
||||
|
||||
.wrap {
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.item-meta { font-size: 22rpx; color: #666; margin-top: 4rpx }
|
||||
|
||||
.wrap { padding: 30rpx; }
|
||||
|
||||
/* 顶部 Tab */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 88rpx;
|
||||
background: rgba($bg-card, 0.9);
|
||||
backdrop-filter: blur(20rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.select-all .checkbox {
|
||||
margin-right: 12rpx;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
box-shadow: $shadow-sm;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: #fff;
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.header { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
|
||||
.status-text { text-align: center; color: #999; margin-top: 100rpx; }
|
||||
|
||||
.inventory-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.inventory-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 24rpx;
|
||||
margin-bottom: 0;
|
||||
border-radius: 8rpx;
|
||||
background-color: #f5f5f5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-size: 24rpx;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.checkbox-area {
|
||||
padding: 10rpx 20rpx 10rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 2rpx solid #ccc;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
background-color: #007AFF;
|
||||
border-color: #007AFF;
|
||||
}
|
||||
|
||||
.checkbox.checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -60%) rotate(45deg);
|
||||
width: 10rpx;
|
||||
height: 20rpx;
|
||||
border-right: 4rpx solid #fff;
|
||||
border-bottom: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.step-btn {
|
||||
width: 48rpx;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f8f8;
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
color: $text-sub;
|
||||
transition: all 0.3s;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&.active {
|
||||
color: $brand-primary;
|
||||
font-weight: 700;
|
||||
font-size: 30rpx;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: $brand-primary;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-btn.minus { border-right: 1px solid #ddd; }
|
||||
.step-btn.plus { border-left: 1px solid #ddd; }
|
||||
.tab-count {
|
||||
margin-left: 8rpx;
|
||||
font-size: 20rpx;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
color: $brand-primary;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 60rpx;
|
||||
/* 状态提示 */
|
||||
.status-text {
|
||||
padding-top: 200rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
color: $text-tertiary;
|
||||
font-size: 28rpx;
|
||||
margin-top: 0;
|
||||
}
|
||||
.loading-more, .no-more {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
color: $text-tertiary;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* Tab 0: 待处理列表 */
|
||||
.action-bar {
|
||||
margin-top: 108rpx; /* 88rpx tabs + 20rpx spacing */
|
||||
padding: 0 $spacing-lg;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.inventory-grid {
|
||||
padding: 0 $spacing-lg;
|
||||
margin-top: 108rpx; /* default margin if no action bar */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
/* Adjust margin if action bar exists */
|
||||
.action-bar + .inventory-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.inventory-item {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 0; /* handled by gap */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
box-shadow: $shadow-sm;
|
||||
border: 1rpx solid rgba(0,0,0,0.02);
|
||||
transition: all 0.2s ease;
|
||||
animation: fadeInUp 0.4s ease-out backwards;
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
&:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-area {
|
||||
padding: 20rpx 20rpx 20rpx 0;
|
||||
}
|
||||
.checkbox {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 2rpx solid $text-tertiary;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.checked {
|
||||
background: $brand-primary;
|
||||
border-color: $brand-primary;
|
||||
position: relative;
|
||||
box-shadow: 0 0 10rpx rgba($brand-primary, 0.3);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 18rpx;
|
||||
height: 10rpx;
|
||||
border-left: 3rpx solid #fff;
|
||||
border-bottom: 3rpx solid #fff;
|
||||
transform: translate(-50%, -65%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: $radius-md;
|
||||
background: $bg-page;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0; /* reset old margin */
|
||||
}
|
||||
.item-info {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 140rpx;
|
||||
text-align: left; /* reset old text-align */
|
||||
}
|
||||
.item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
@include text-ellipsis(2);
|
||||
margin-bottom: 0; /* reset old margin */
|
||||
}
|
||||
.item-price {
|
||||
font-size: 24rpx;
|
||||
color: $brand-primary;
|
||||
margin-top: 8rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.item-actions {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.item-count {
|
||||
font-size: 28rpx;
|
||||
color: $text-main;
|
||||
font-weight: 600;
|
||||
}
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-sm;
|
||||
padding: 4rpx;
|
||||
height: auto; /* reset old height */
|
||||
border: none; /* reset old border */
|
||||
}
|
||||
.step-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: $text-main;
|
||||
background: transparent;
|
||||
|
||||
&.minus { color: $text-sub; border: none; }
|
||||
&.plus { color: $brand-primary; border: none; }
|
||||
|
||||
&:active { opacity: 0.6; background: transparent !important; transform: scale(0.9); }
|
||||
}
|
||||
.step-num {
|
||||
min-width: 60rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100rpx;
|
||||
background-color: #fff;
|
||||
background: rgba($bg-card, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
|
||||
z-index: 100;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
||||
z-index: 99;
|
||||
height: auto; /* reset old height */
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
color: $text-main;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin: 0;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
font-size: 26rpx;
|
||||
border-radius: 32rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 40rpx;
|
||||
border-radius: $radius-round;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
margin: 0;
|
||||
line-height: 1; /* reset line-height */
|
||||
|
||||
&:active { transform: scale(0.96); }
|
||||
}
|
||||
|
||||
.btn-ship {
|
||||
background-color: #f0ad4e;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
color: $brand-primary;
|
||||
box-shadow: none; /* reset shadow */
|
||||
}
|
||||
.btn-redeem {
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
box-shadow: $shadow-warm;
|
||||
animation: none; /* reset animation */
|
||||
}
|
||||
|
||||
.btn-redeem {
|
||||
background-color: #dd524d;
|
||||
color: #fff;
|
||||
/* Tab 1: 已申请发货 */
|
||||
.shipment-list {
|
||||
padding: 0 $spacing-lg;
|
||||
margin-top: 108rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.shipment-card {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 0; /* handled by gap */
|
||||
box-shadow: $shadow-sm;
|
||||
border: 1rpx solid rgba(0,0,0,0.02);
|
||||
animation: fadeInUp 0.4s ease-out backwards;
|
||||
}
|
||||
.shipment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.shipment-batch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.batch-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
}
|
||||
.batch-no {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
font-family: monospace;
|
||||
}
|
||||
.count-badge {
|
||||
font-size: 20rpx;
|
||||
color: $brand-primary;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.shipment-status {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
&.status-pending { background: #FFF7E6; color: #FA8C16; }
|
||||
&.status-shipped { background: #E6F7FF; color: #1890FF; }
|
||||
&.status-delivered { background: #F6FFED; color: #52C41A; }
|
||||
&.status-cancelled { background: #F5F5F5; color: #999; }
|
||||
}
|
||||
|
||||
.product-thumbnails {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.thumb-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.thumb-img {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: $radius-sm;
|
||||
background: $bg-page;
|
||||
flex-shrink: 0;
|
||||
border: 1rpx solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.thumb-more {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: $radius-sm;
|
||||
background: $bg-page;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $text-sub;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.product-names {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
line-height: 1.4;
|
||||
display: block; /* reset flex */
|
||||
}
|
||||
.product-name-item {
|
||||
margin-right: 12rpx;
|
||||
background: transparent; /* reset bg */
|
||||
padding: 0;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '、';
|
||||
}
|
||||
}
|
||||
|
||||
.shipment-express {
|
||||
background: $bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.express-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.express-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.express-company {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
}
|
||||
.express-no {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.express-copy {
|
||||
font-size: 22rpx;
|
||||
color: $brand-primary;
|
||||
padding: 6rpx 16rpx;
|
||||
border: 1rpx solid $brand-primary;
|
||||
border-radius: 20rpx;
|
||||
background: transparent; /* reset bg */
|
||||
box-shadow: none; /* reset shadow */
|
||||
|
||||
&:active { background: rgba($brand-primary, 0.05); }
|
||||
}
|
||||
|
||||
.shipment-time {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
border-top: 1rpx dashed rgba(0,0,0,0.05);
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
.time-item {
|
||||
font-size: 22rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 120rpx;
|
||||
height: calc(120rpx + constant(safe-area-inset-bottom));
|
||||
height: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,44 +1,128 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="notice-bar">
|
||||
<swiper class="notice-swiper" vertical circular autoplay interval="3000" duration="300">
|
||||
<swiper-item v-for="n in displayNotices" :key="n.id">
|
||||
<view class="notice-item">{{ n.text }}</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- 背景装饰 -->
|
||||
<view class="bg-decoration"></view>
|
||||
|
||||
<!-- 顶部导航栏 (搜索) -->
|
||||
<view class="nav-header">
|
||||
<view class="brand-logo">
|
||||
<text class="brand-text">奇盒潮玩</text>
|
||||
<view class="brand-star">✨</view>
|
||||
</view>
|
||||
<view class="search-bar">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">搜索商品</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="banner-box">
|
||||
<swiper class="banner-swiper" circular autoplay interval="4000" duration="500">
|
||||
<swiper-item v-for="b in displayBanners" :key="b.id">
|
||||
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
|
||||
<view v-else class="banner-fallback">
|
||||
<text class="banner-fallback-text">{{ b.title || '敬请期待' }}</text>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<view class="activity-section">
|
||||
<view class="section-title">活动</view>
|
||||
<view v-if="activityGroups.length">
|
||||
<scroll-view class="tabs" scroll-x>
|
||||
<view class="tab" v-for="g in activityGroups" :key="g.name" :class="{ active: g.name === selectedGroupName }" @tap="onSelectGroup(g.name)">{{ g.name }}</view>
|
||||
</scroll-view>
|
||||
<view v-if="activeGroupItems.length" class="activity-grid">
|
||||
<view class="activity-item" v-for="a in activeGroupItems" :key="a.id" @tap="onActivityTap(a)">
|
||||
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
|
||||
<!-- 滚动区域 -->
|
||||
<scroll-view class="main-content" scroll-y>
|
||||
<!-- Banner 区域 -->
|
||||
<view class="banner-box">
|
||||
<swiper class="banner-swiper" circular autoplay interval="4000" duration="500">
|
||||
<swiper-item v-for="b in displayBanners" :key="b.id">
|
||||
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
|
||||
<view v-else class="banner-fallback">
|
||||
<text class="banner-fallback-text">{{ a.title || '活动敬请期待' }}</text>
|
||||
<text class="banner-fallback-text">{{ b.title || '奇盒潮玩 V6.0' }}</text>
|
||||
<text class="banner-tag">功能更新UI优化全面来袭</text>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 通知栏 -->
|
||||
<view class="notice-bar" @tap="onNoticeTap">
|
||||
<view class="notice-tag">通知</view>
|
||||
<swiper class="notice-swiper" vertical circular autoplay interval="3000" duration="300">
|
||||
<swiper-item v-for="n in displayNotices" :key="n.id">
|
||||
<view class="notice-item">{{ n.text }}</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="notice-more">查看详情</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- 玩法分类专区 -->
|
||||
<view class="gameplay-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">玩法分类</text>
|
||||
</view>
|
||||
<view class="gameplay-grid-v2">
|
||||
<!-- 上排:两大核心 -->
|
||||
<view class="grid-row-top">
|
||||
<view class="game-card-large card-yifan" @tap="navigateTo('/pages/activity/list/index?category=一番赏')">
|
||||
<view class="card-bg-decoration"></view>
|
||||
<view class="card-content-large">
|
||||
<text class="card-title-large">一番赏</text>
|
||||
<view class="card-tag-large">欧皇擂台</view>
|
||||
<image class="card-mascot-large" src="https://via.placeholder.com/150/90EE90/000000?text=YI" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="game-card-large card-wuxian" @tap="navigateTo('/pages/activity/list/index?category=无限赏')">
|
||||
<view class="card-content-large">
|
||||
<text class="card-title-large">无限赏</text>
|
||||
<view class="card-tag-large yellow">一发入魂</view>
|
||||
<image class="card-mascot-large" src="https://via.placeholder.com/150/FFD700/000000?text=WU" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下排:三小功能 -->
|
||||
<view class="grid-row-bottom">
|
||||
<view class="game-card-small card-match" @tap="navigateTo('/pages/activity/list/index?category=对对碰')">
|
||||
<text class="card-title-small">对对碰</text>
|
||||
<text class="card-subtitle-small">碰一碰消除</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/FFB6C1/000000?text=Match" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="game-card-small card-tower" @tap="navigateTo('/pages/activity/list/index?category=爬塔')">
|
||||
<text class="card-title-small">爬塔</text>
|
||||
<text class="card-subtitle-small">层层挑战</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Tower" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="game-card-small card-more" @tap="navigateTo('#')">
|
||||
<text class="card-title-small">更多</text>
|
||||
<text class="card-subtitle-small">敬请期待</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/E0E0E0/000000?text=More" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="activity-name">{{ a.title }}</text>
|
||||
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="activity-empty">该分组暂无活动</view>
|
||||
</view>
|
||||
<view v-else class="activity-empty">暂无活动</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐活动列表 -->
|
||||
<view class="activity-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">推荐活动</text>
|
||||
</view>
|
||||
|
||||
<view v-if="activeGroupItems.length" class="activity-grid-list">
|
||||
<view class="activity-item" v-for="a in activeGroupItems" :key="a.id" @tap="onActivityTap(a)">
|
||||
<view class="activity-thumb-box">
|
||||
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
|
||||
<view v-else class="banner-fallback mini">
|
||||
<text class="banner-fallback-text mini">{{ a.title }}</text>
|
||||
</view>
|
||||
<!-- 热门标签 -->
|
||||
<view class="activity-tag-hot">HOT</view>
|
||||
</view>
|
||||
<view class="activity-info">
|
||||
<text class="activity-name">{{ a.title }}</text>
|
||||
<view class="activity-row">
|
||||
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
|
||||
<view class="activity-btn-go">GO</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="activity-empty">暂无更多活动</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部垫高 -->
|
||||
<view style="height: 40rpx"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -80,10 +164,8 @@ export default {
|
||||
return Array.from(map.entries()).map(([name, items]) => ({ name, items }))
|
||||
},
|
||||
activeGroupItems() {
|
||||
const groups = this.activityGroups
|
||||
const name = this.selectedGroupName || (groups[0] && groups[0].name) || ''
|
||||
const g = groups.find(x => x.name === name)
|
||||
return g ? g.items : []
|
||||
// Return ALL activities without filtering by group
|
||||
return Array.isArray(this.activities) ? this.activities : []
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@ -109,11 +191,7 @@ export default {
|
||||
this.selectedGroupName = String(name || '')
|
||||
},
|
||||
updateSelectedGroup() {
|
||||
const groups = this.activityGroups
|
||||
if (!groups.length) { this.selectedGroupName = ''; return }
|
||||
if (!groups.find(g => g.name === this.selectedGroupName)) {
|
||||
this.selectedGroupName = groups[0].name
|
||||
}
|
||||
// No-op as we now show all
|
||||
},
|
||||
toArray(x) { return Array.isArray(x) ? x : [] },
|
||||
unwrap(list) {
|
||||
@ -142,7 +220,6 @@ export default {
|
||||
},
|
||||
normalizeBanners(list) {
|
||||
const arr = this.unwrap(list)
|
||||
console.log('normalizeBanners input', list, 'unwrapped', arr)
|
||||
const mapped = arr.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
title: i.title ?? '',
|
||||
@ -151,12 +228,10 @@ export default {
|
||||
sort: typeof i.sort === 'number' ? i.sort : 0
|
||||
})).filter(i => i.image)
|
||||
mapped.sort((a, b) => a.sort - b.sort)
|
||||
console.log('normalizeBanners mapped', mapped)
|
||||
return mapped
|
||||
},
|
||||
normalizeActivities(list) {
|
||||
const arr = this.unwrap(list)
|
||||
console.log('normalizeActivities input', list, 'unwrapped', arr)
|
||||
const mapped = arr.map((i, idx) => ({
|
||||
id: i.id ?? String(idx),
|
||||
image: this.cleanUrl(i.image ?? i.banner ?? i.coverUrl ?? i.cover_url ?? i.img ?? i.pic ?? ''),
|
||||
@ -166,7 +241,6 @@ export default {
|
||||
category_name: (i.category_name ?? i.categoryName ?? '').trim(),
|
||||
category_id: i.activity_category_id ?? i.category_id ?? i.categoryId ?? null
|
||||
})).filter(i => i.image || i.title)
|
||||
console.log('normalizeActivities mapped', mapped)
|
||||
return mapped
|
||||
},
|
||||
buildActivitySubtitle(i) {
|
||||
@ -178,36 +252,29 @@ export default {
|
||||
return parts.join(' · ')
|
||||
},
|
||||
async loadHomeData() {
|
||||
const results = await Promise.allSettled([
|
||||
this.apiGet('/api/app/notices'),
|
||||
this.apiGet('/api/app/banners'),
|
||||
this.apiGet('/api/app/activities')
|
||||
])
|
||||
const [nRes, bRes, acRes] = results
|
||||
if (nRes.status === 'fulfilled') {
|
||||
console.log('notices ok', nRes.value)
|
||||
this.notices = this.normalizeNotices(nRes.value)
|
||||
} else {
|
||||
console.error('notices error', nRes.reason)
|
||||
// Notices
|
||||
try {
|
||||
const nData = await this.apiGet('/api/app/notices')
|
||||
this.notices = this.normalizeNotices(nData)
|
||||
} catch (e) {
|
||||
this.notices = []
|
||||
}
|
||||
if (bRes.status === 'fulfilled') {
|
||||
console.log('banners ok', bRes.value)
|
||||
this.banners = this.normalizeBanners(bRes.value)
|
||||
} else {
|
||||
console.error('banners error', bRes.reason)
|
||||
|
||||
// Banners
|
||||
try {
|
||||
const bData = await this.apiGet('/api/app/banners')
|
||||
this.banners = this.normalizeBanners(bData)
|
||||
} catch (e) {
|
||||
this.banners = []
|
||||
}
|
||||
if (acRes.status === 'fulfilled') {
|
||||
console.log('activities ok', acRes.value)
|
||||
this.activities = this.normalizeActivities(acRes.value)
|
||||
this.updateSelectedGroup()
|
||||
} else {
|
||||
console.error('activities error', acRes.reason)
|
||||
|
||||
// Activities
|
||||
try {
|
||||
const acData = await this.apiGet('/api/app/activities')
|
||||
this.activities = this.normalizeActivities(acData)
|
||||
} catch (e) {
|
||||
this.activities = []
|
||||
this.updateSelectedGroup()
|
||||
}
|
||||
console.log('home normalized', { notices: this.notices, banners: this.banners, activities: this.activities })
|
||||
},
|
||||
onBannerTap(b) {
|
||||
const imgs = (Array.isArray(this.banners) ? this.banners : []).map(x => x.image).filter(Boolean)
|
||||
@ -224,9 +291,11 @@ export default {
|
||||
const name = (a.category_name || a.categoryName || '').trim()
|
||||
const id = a.id
|
||||
let path = ''
|
||||
if (name === '一番赏') path = '/pages/activity/yifanshang/index'
|
||||
else if (name === '无限赏') path = '/pages/activity/wuxianshang/index'
|
||||
else if (name === '对对碰') path = '/pages/activity/duiduipeng/index'
|
||||
if (name.includes('一番赏')) path = '/pages/activity/yifanshang/index'
|
||||
else if (name.includes('无限赏')) path = '/pages/activity/wuxianshang/index'
|
||||
else if (name.includes('对对碰')) path = '/pages/activity/duiduipeng/index'
|
||||
else if (name.includes('爬塔')) path = '/pages/activity/pata/index'
|
||||
|
||||
if (path && id) {
|
||||
uni.navigateTo({ url: `${path}?id=${id}` })
|
||||
return
|
||||
@ -234,29 +303,652 @@ export default {
|
||||
if (a.link && /^\/.+/.test(a.link)) {
|
||||
uni.navigateTo({ url: a.link })
|
||||
}
|
||||
},
|
||||
navigateTo(url) {
|
||||
if(url === '#') return
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
onNoticeTap() {
|
||||
const content = this.displayNotices.map(n => n.text).join('\n')
|
||||
uni.showModal({
|
||||
title: '系统通知',
|
||||
content: content || '暂无通知',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}
|
||||
},
|
||||
// 分享给好友
|
||||
onShareAppMessage() {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: '奇盒潮玩 - 开箱惊喜等你来',
|
||||
path: `/pages/index/index?invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
},
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: '奇盒潮玩 - 开箱惊喜等你来',
|
||||
query: `invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { padding: 24rpx }
|
||||
.notice-bar { height: 64rpx; background: #fff4e6; border-radius: 8rpx; overflow: hidden; margin-bottom: 24rpx; }
|
||||
.notice-swiper { height: 64rpx }
|
||||
.notice-item { height: 64rpx; line-height: 64rpx; padding: 0 24rpx; color: #a15c00; font-size: 26rpx }
|
||||
.banner-box { margin-bottom: 24rpx }
|
||||
.banner-swiper { width: 100%; height: 320rpx; border-radius: 12rpx; overflow: hidden }
|
||||
.banner-image { width: 100%; height: 320rpx }
|
||||
.banner-fallback { width: 100%; height: 320rpx; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f5f5f5, #eaeaea) }
|
||||
.banner-fallback-text { color: #666; font-size: 28rpx }
|
||||
.activity-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx }
|
||||
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
|
||||
.tabs { white-space: nowrap; display: flex; gap: 12rpx; margin-bottom: 16rpx }
|
||||
.tab { display: inline-flex; align-items: center; height: 56rpx; padding: 0 20rpx; border-radius: 999rpx; background: #f5f7fa; color: #555; font-size: 26rpx }
|
||||
.tab.active { background: #007AFF; color: #fff }
|
||||
.activity-grid { display: flex; flex-wrap: wrap; margin: -12rpx }
|
||||
.activity-item { width: 50%; padding: 12rpx }
|
||||
.activity-thumb { width: 100%; height: 200rpx; border-radius: 8rpx }
|
||||
.activity-name { display: block; margin-top: 8rpx; font-size: 26rpx; color: #222 }
|
||||
.activity-desc { display: block; margin-top: 4rpx; font-size: 22rpx; color: #888 }
|
||||
<style lang="scss">
|
||||
/* ============================================
|
||||
奇盒潮玩 - 首页样式 (V6.0 Pro Refined)
|
||||
============================================ */
|
||||
|
||||
.page {
|
||||
padding: 0;
|
||||
background-color: $bg-page;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========== 顶部导航栏 ========== */
|
||||
.nav-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
background: transparent; /* 透明背景,依靠内容区的渐变 */
|
||||
padding-top: calc($spacing-md + env(safe-area-inset-top));
|
||||
gap: $spacing-lg;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
font-style: italic;
|
||||
letter-spacing: -1rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.brand-star {
|
||||
font-size: 24rpx;
|
||||
margin-left: 4rpx;
|
||||
margin-top: -16rpx;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: $radius-round;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 $spacing-lg;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow: $shadow-sm;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-bar:active {
|
||||
background: #fff;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.search-icon { margin-right: 12rpx; font-size: 32rpx; }
|
||||
.search-placeholder { color: $text-tertiary; font-size: 28rpx; font-weight: 500; }
|
||||
|
||||
|
||||
/* ========== 滚动主内容区 ========== */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
/* background: linear-gradient(180deg, $bg-secondary 0%, $bg-page 400rpx); */ /* 移除原有背景,使用全局背景装饰 */
|
||||
position: relative;
|
||||
z-index: 1; /* 确保内容在背景装饰之上 */
|
||||
padding-top: $spacing-sm;
|
||||
}
|
||||
|
||||
/* Logo Banner */
|
||||
.banner-box {
|
||||
margin: 0 $spacing-lg $spacing-lg;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-float;
|
||||
transform: translateZ(0); /* 开启硬件加速 */
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.banner-swiper, .banner-image, .banner-fallback {
|
||||
width: 100%;
|
||||
height: 340rpx; /* 略微增高 */
|
||||
}
|
||||
|
||||
.banner-fallback {
|
||||
background: linear-gradient(135deg, $bg-secondary, #FFF0D6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 4rpx dashed $brand-primary-light;
|
||||
}
|
||||
|
||||
.banner-fallback-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
color: $brand-primary;
|
||||
font-style: italic;
|
||||
margin-bottom: 12rpx;
|
||||
text-shadow: 0 2rpx 0 rgba(255,255,255,1);
|
||||
}
|
||||
|
||||
.banner-tag {
|
||||
background: #1A1A1A;
|
||||
color: $accent-gold;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: $radius-round;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 通知栏 */
|
||||
.notice-bar {
|
||||
margin: 0 $spacing-lg $spacing-xl;
|
||||
background: #FFFFFF;
|
||||
border-radius: $radius-round;
|
||||
padding: 16rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
box-shadow: $shadow-sm;
|
||||
animation: fadeInUp 0.6s ease-out 0.1s backwards;
|
||||
}
|
||||
|
||||
.notice-tag {
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2rpx 6rpx rgba($brand-primary, 0.3);
|
||||
}
|
||||
|
||||
.notice-swiper { flex: 1; height: 36rpx; }
|
||||
.notice-item {
|
||||
font-size: 26rpx;
|
||||
color: $text-main;
|
||||
line-height: 36rpx;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
}
|
||||
.notice-more { font-size: 24rpx; color: $text-sub; display: flex; align-items: center; }
|
||||
.notice-more::after { content: '›'; margin-left: 4rpx; font-size: 32rpx; line-height: 24rpx; }
|
||||
|
||||
|
||||
/* 玩法专区 - 方案B:2+3 杂志风布局 */
|
||||
.gameplay-section {
|
||||
padding: 0 $spacing-lg;
|
||||
margin-bottom: $spacing-xl;
|
||||
animation: fadeInUp 0.6s ease-out 0.2s backwards;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-left: 12rpx;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 标题装饰竖线 */
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) skewX(-10deg);
|
||||
width: 8rpx;
|
||||
height: 32rpx;
|
||||
background: $brand-primary;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.gameplay-grid-v2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 上排 */
|
||||
.grid-row-top {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
height: 220rpx; /* 增加高度,展示更多细节 */
|
||||
}
|
||||
|
||||
.game-card-large {
|
||||
flex: 1;
|
||||
border-radius: $radius-lg;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 28rpx;
|
||||
box-shadow: $shadow-card;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.game-card-large:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 下排 */
|
||||
.grid-row-bottom {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.game-card-small {
|
||||
flex: 1;
|
||||
border-radius: $radius-md;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-sm;
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.game-card-small:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 内容样式 - 大卡片 */
|
||||
.card-content-large {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-title-large {
|
||||
font-size: 40rpx;
|
||||
font-weight: 900;
|
||||
color: #FFF;
|
||||
font-style: italic;
|
||||
margin-bottom: 16rpx;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card-tag-large {
|
||||
font-size: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: $text-main;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.card-tag-large.yellow { color: #D97706; }
|
||||
|
||||
.card-mascot-large {
|
||||
position: absolute;
|
||||
right: -20rpx;
|
||||
bottom: -30rpx;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
transform: rotate(10deg);
|
||||
filter: drop-shadow(0 8rpx 16rpx rgba(0,0,0,0.2));
|
||||
}
|
||||
|
||||
/* 内容样式 - 小卡片 */
|
||||
.card-title-small {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
margin-bottom: 6rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card-subtitle-small {
|
||||
font-size: 22rpx;
|
||||
color: $text-sub;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card-icon-small {
|
||||
position: absolute;
|
||||
right: -10rpx;
|
||||
bottom: -10rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
opacity: 0.9;
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
|
||||
/* 背景配色 - 优化后的渐变 */
|
||||
.card-yifan {
|
||||
background: linear-gradient(135deg, $brand-primary 0%, $brand-secondary 100%); /* 品牌橙渐变 */
|
||||
}
|
||||
|
||||
.card-wuxian {
|
||||
background: $gradient-gold; /* 质感金渐变 */
|
||||
}
|
||||
|
||||
.card-match {
|
||||
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%); /* 柔和粉 */
|
||||
}
|
||||
.card-match .card-title-small { color: $accent-pink; }
|
||||
|
||||
.card-tower {
|
||||
background: linear-gradient(135deg, #E0C3FC 0%, #8EC5FC 100%); /* 梦幻紫蓝 */
|
||||
}
|
||||
.card-tower .card-title-small { color: $accent-purple; }
|
||||
|
||||
.card-more {
|
||||
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
|
||||
}
|
||||
.card-more .card-title-small { color: $text-sub; }
|
||||
|
||||
|
||||
/* 推荐活动列表 */
|
||||
.activity-section {
|
||||
padding: 0 $spacing-lg;
|
||||
animation: fadeInUp 0.6s ease-out 0.3s backwards;
|
||||
}
|
||||
.activity-grid-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
background: #FFFFFF;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.activity-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.activity-thumb-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 100%; /* 1:1 正方形 */
|
||||
}
|
||||
.activity-thumb {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
.activity-tag-hot {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
left: 16rpx;
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
padding: 24rpx;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 20rpx;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.activity-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-size: 26rpx;
|
||||
color: $accent-red; /* 价格/热度颜色 */
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.activity-btn-go {
|
||||
background: $text-main;
|
||||
color: #FFD700;
|
||||
font-size: 20rpx;
|
||||
font-weight: 900;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: $radius-round;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.activity-empty {
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
color: $text-tertiary;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.9; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8rpx); }
|
||||
}
|
||||
|
||||
/* Banner 入场动画 */
|
||||
.banner-box {
|
||||
animation: scaleIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* 通知栏滑入 */
|
||||
.notice-bar {
|
||||
animation: fadeInUp 0.4s ease-out 0.1s both;
|
||||
}
|
||||
|
||||
/* 玩法卡片交错入场 */
|
||||
.game-card-large:first-child {
|
||||
animation: fadeInUp 0.4s ease-out 0.2s both;
|
||||
}
|
||||
.game-card-large:last-child {
|
||||
animation: fadeInUp 0.4s ease-out 0.3s both;
|
||||
}
|
||||
.game-card-small:nth-child(1) {
|
||||
animation: fadeInUp 0.4s ease-out 0.35s both;
|
||||
}
|
||||
.game-card-small:nth-child(2) {
|
||||
animation: fadeInUp 0.4s ease-out 0.4s both;
|
||||
}
|
||||
.game-card-small:nth-child(3) {
|
||||
animation: fadeInUp 0.4s ease-out 0.45s both;
|
||||
}
|
||||
|
||||
/* 活动卡片交错入场 */
|
||||
.activity-item:nth-child(1) { animation: fadeInUp 0.4s ease-out 0.2s both; }
|
||||
.activity-item:nth-child(2) { animation: fadeInUp 0.4s ease-out 0.25s both; }
|
||||
.activity-item:nth-child(3) { animation: fadeInUp 0.4s ease-out 0.3s both; }
|
||||
.activity-item:nth-child(4) { animation: fadeInUp 0.4s ease-out 0.35s both; }
|
||||
.activity-item:nth-child(n+5) { animation: fadeInUp 0.4s ease-out 0.4s both; }
|
||||
|
||||
/* 玻璃态搜索栏 */
|
||||
.search-bar {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20rpx);
|
||||
-webkit-backdrop-filter: blur(20rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.search-bar:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 卡片点击效果 */
|
||||
.game-card-large,
|
||||
.game-card-small {
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
cursor: pointer;
|
||||
}
|
||||
.game-card-large:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
.game-card-small:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* 活动卡片悬浮效果 */
|
||||
.activity-item {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
.activity-item:active {
|
||||
transform: scale(0.97) translateY(4rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* GO 按钮脉冲动画 */
|
||||
.activity-btn-go {
|
||||
animation: pulse 2s infinite;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.activity-btn-go:active {
|
||||
animation: none;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* 图片加载动画 */
|
||||
.activity-thumb {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
/* 热门标签浮动效果 */
|
||||
.activity-tag-hot {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Banner 图片悬浮效果 */
|
||||
.banner-image {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
.banner-box:active .banner-image {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 通知栏点击效果 */
|
||||
.notice-bar {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.notice-bar:active {
|
||||
background: #F0F0F0;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
/* 品牌标志动画 */
|
||||
.brand-star {
|
||||
animation: float 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Section 标题增强 */
|
||||
.section-header {
|
||||
animation: fadeInUp 0.4s ease-out 0.15s both;
|
||||
}
|
||||
|
||||
/* 增强阴影效果 */
|
||||
.banner-box {
|
||||
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
.activity-item {
|
||||
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* 热门标签增强 */
|
||||
.activity-tag-hot {
|
||||
background: linear-gradient(135deg, rgba(255,77,79,0.9), rgba(255,107,53,0.9));
|
||||
color: #fff;
|
||||
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,74 +1,105 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
||||
<!-- #ifdef MP-TOUTIAO -->
|
||||
<view class="login-form">
|
||||
<view class="input-row">
|
||||
<text class="label">账号:</text>
|
||||
<input
|
||||
type="text"
|
||||
v-model="account"
|
||||
class="input-field"
|
||||
placeholder="请输入账号"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-row">
|
||||
<text class="label">密码:</text>
|
||||
<input
|
||||
type="password"
|
||||
v-model="pwd"
|
||||
class="input-field"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 记住账号密码 -->
|
||||
<view class="remember-row">
|
||||
<checkbox :value="remember" @change="handleRememberChange" />
|
||||
<text class="remember-text">记住账号密码</text>
|
||||
</view>
|
||||
<!-- 按钮区域 -->
|
||||
<view class="button-group">
|
||||
<button class="btn login-btn" @click="handleLogin">登录</button>
|
||||
</view>
|
||||
<!-- 注册链接 -->
|
||||
<view class="register-link">
|
||||
<text class="register-text" @click="goToRegister">还没有账号?点击注册</text>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="title">微信登录</view>
|
||||
|
||||
|
||||
<button class="btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">授权手机号快速登录</button>
|
||||
<!-- #endif -->
|
||||
<view class="agreements">
|
||||
<text>注册或登录即表示您已阅读并同意</text>
|
||||
<text class="link" @tap="toUserAgreement">《用户协议》</text>
|
||||
<text>和</text>
|
||||
<text class="link" @tap="toPurchaseAgreement">《购买协议》</text>
|
||||
<!-- 装饰球体 -->
|
||||
<view class="orb orb-1"></view>
|
||||
<view class="orb orb-2"></view>
|
||||
|
||||
<view class="content-wrap">
|
||||
<view class="glass-card">
|
||||
<!-- 品牌Logo -->
|
||||
<view class="brand-section">
|
||||
<view class="logo-box">
|
||||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
||||
</view>
|
||||
<view class="app-name">奇盒潮玩</view>
|
||||
<view class="welcome-text">开启欧气之旅 ✨</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<!-- #ifdef MP-TOUTIAO -->
|
||||
<view class="login-form">
|
||||
<view class="input-group">
|
||||
<view class="input-icon">
|
||||
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjAgMjF2LTJhNCA0IDAgMCAwLTQtNEg4YTQgNCAwIDAgMC00IDR2MiIgLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiIC8+PC9zdmc+" mode="aspectFit"></image>
|
||||
</view>
|
||||
<input
|
||||
type="text"
|
||||
v-model="account"
|
||||
class="input-field"
|
||||
placeholder="请输入账号"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<view class="input-icon">
|
||||
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB4PSIzIiB5PS“xMSIgd2lkdGg9“MTgiIGhlaWdodD0iMTEiIHJ4PSIyIiByeT0iMiIgLz48cGF0aCBkPSJNNyAxMVY3YTUgNSAwIDAgMSAxMCAwdjQiIC8+PC9zdmc+" mode="aspectFit"></image>
|
||||
</view>
|
||||
<input
|
||||
type="password"
|
||||
v-model="pwd"
|
||||
class="input-field"
|
||||
placeholder="请输入密码"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="options-row">
|
||||
<view class="remember-box" @click="toggleRemember">
|
||||
<view class="checkbox" :class="{ checked: remember }">
|
||||
<view class="check-mark" v-if="remember">✓</view>
|
||||
</view>
|
||||
<text class="remember-text">记住密码</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn login-btn" @click="handleLogin">
|
||||
<text class="btn-text">立即登录</text>
|
||||
<view class="btn-shine"></view>
|
||||
</button>
|
||||
|
||||
<view class="register-link">
|
||||
<text class="register-text" @click="goToRegister">没有账号?<text class="highlight">立即注册</text></text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="weixin-login-box">
|
||||
<button class="btn weixin-btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">
|
||||
<image class="wx-icon" src="/static/logo.png" mode="aspectFit"></image> <!-- 应该用微信图标,暂时用logo代替或SVG -->
|
||||
<text>微信一键登录</text>
|
||||
</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 协议区 -->
|
||||
<view class="agreements">
|
||||
<view class="checkbox-area">
|
||||
<view class="checkbox round" :class="{ checked: agreementChecked }" @click="toggleAgreement"></view>
|
||||
</view>
|
||||
<view class="agreement-text">
|
||||
登录即代表同意 <text class="link" @tap="toUserAgreement">用户协议</text> & <text class="link" @tap="toPurchaseAgreement">隐私政策</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="error" class="error-toast">{{ error }}</view>
|
||||
</view>
|
||||
<view v-if="needBindPhone" class="tip">登录成功,请绑定手机号以完成登录</view>
|
||||
<view v-if="error" class="error">{{ error }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed,onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const needBindPhone = ref(false)
|
||||
const account =ref("")
|
||||
const pwd = ref ("")
|
||||
const remember=ref(false)
|
||||
const loggedIn = computed(() => !!uni.getStorageSync('token'))
|
||||
const account = ref("")
|
||||
const pwd = ref("")
|
||||
const remember = ref(false)
|
||||
const agreementChecked = ref(false) // 默认为false,需要用户勾选
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
@ -83,120 +114,97 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 切换“记住密码”
|
||||
const handleRememberChange = (e) => {
|
||||
remember.value = e.detail.value.length > 0
|
||||
function toggleRemember() {
|
||||
remember.value = !remember.value
|
||||
}
|
||||
|
||||
|
||||
|
||||
function toggleAgreement() {
|
||||
agreementChecked.value = !agreementChecked.value
|
||||
}
|
||||
|
||||
function goToRegister() { uni.navigateTo({ url: '/pages/register/register' }) }
|
||||
function onLogin() {}
|
||||
|
||||
function handleLogin() {
|
||||
if (!agreementChecked.value) {
|
||||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// TODO: Implement actual username/password login logic if API available
|
||||
uni.showToast({ title: '普通登录逻辑待接入', icon: 'none' })
|
||||
}
|
||||
|
||||
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
|
||||
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/agreement/purchase' }) }
|
||||
|
||||
function onGetPhoneNumber(e) {
|
||||
if (!agreementChecked.value) {
|
||||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const phoneCode = e.detail.code
|
||||
console.log('login_flow start getPhoneNumber, codeExists:', !!phoneCode)
|
||||
if (!phoneCode) {
|
||||
uni.showToast({ title: '未授权手机号', icon: 'none' })
|
||||
console.error('login_flow error: missing phoneCode')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: async (res) => {
|
||||
try {
|
||||
const loginCode = res.code
|
||||
console.log('login_flow uni.login success, loginCode exists:', !!loginCode)
|
||||
const inviterCode = uni.getStorageSync('inviter_code')
|
||||
console.log('login_flow using inviter_code:', inviterCode)
|
||||
const data = await wechatLogin(loginCode, inviterCode)
|
||||
console.log('login_flow wechatLogin response user_id:', data && data.user_id)
|
||||
const token = data && data.token
|
||||
const user_id = data && data.user_id
|
||||
const avatar = data && data.avatar
|
||||
const nickname = data && data.nickname
|
||||
const invite_code = data && data.invite_code
|
||||
const openid = data && data.openid
|
||||
uni.setStorageSync('user_info', data || {})
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token)
|
||||
console.log('login_flow token stored')
|
||||
}
|
||||
if (user_id) {
|
||||
uni.setStorageSync('user_id', user_id)
|
||||
console.log('login_flow user_id stored:', user_id)
|
||||
}
|
||||
if (avatar) {
|
||||
uni.setStorageSync('avatar', avatar)
|
||||
}
|
||||
if (nickname) {
|
||||
uni.setStorageSync('nickname', nickname)
|
||||
}
|
||||
if (invite_code) {
|
||||
uni.setStorageSync('invite_code', invite_code)
|
||||
}
|
||||
if (openid) {
|
||||
uni.setStorageSync('openid', openid)
|
||||
}
|
||||
console.log('login_flow bindPhone start')
|
||||
try {
|
||||
// 首次绑定前短暂延迟,确保服务端 token 生效
|
||||
const user_info = data || {}
|
||||
|
||||
uni.setStorageSync('user_info', user_info)
|
||||
if (token) uni.setStorageSync('token', token)
|
||||
if (user_id) uni.setStorageSync('user_id', user_id)
|
||||
if (user_info.avatar) uni.setStorageSync('avatar', user_info.avatar)
|
||||
if (user_info.nickname) uni.setStorageSync('nickname', user_info.nickname)
|
||||
if (user_info.invite_code) uni.setStorageSync('invite_code', user_info.invite_code)
|
||||
// 保存 openid 用于支付等场景
|
||||
const openid = data && (data.openid || data.open_id)
|
||||
if (openid) uni.setStorageSync('openid', openid)
|
||||
|
||||
// 绑定手机号逻辑...
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
const bindRes = await bindPhone(user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
|
||||
const phoneNumber = (bindRes && (bindRes.phone || bindRes.phone_number || bindRes.mobile)) || ''
|
||||
if (phoneNumber) uni.setStorageSync('phone_number', phoneNumber)
|
||||
} catch (bindErr) {
|
||||
if (bindErr && bindErr.statusCode === 401) {
|
||||
console.warn('login_flow bindPhone 401, try re-login and retry')
|
||||
// 重新获取登录 code
|
||||
const relogin = await new Promise((resolve, reject) => {
|
||||
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
||||
})
|
||||
const data2 = await wechatLogin(relogin.code, inviterCode)
|
||||
const token2 = data2 && data2.token
|
||||
const user2 = data2 && data2.user_id
|
||||
if (token2) uni.setStorageSync('token', token2)
|
||||
if (user2) uni.setStorageSync('user_id', user2)
|
||||
// 再次延迟后重试绑定
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
const bindRes2 = await bindPhone(user2 || user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
|
||||
const phoneNumber2 = (bindRes2 && (bindRes2.phone || bindRes2.phone_number || bindRes2.mobile)) || ''
|
||||
if (phoneNumber2) uni.setStorageSync('phone_number', phoneNumber2)
|
||||
} else {
|
||||
throw bindErr
|
||||
}
|
||||
// 忽略绑定失败,允许静默失败或重试逻辑
|
||||
console.warn('Bind phone failed', bindErr)
|
||||
}
|
||||
|
||||
uni.setStorageSync('phone_bound', true)
|
||||
console.log('login_flow bindPhone success, phone_bound stored')
|
||||
|
||||
// 获取用户信息
|
||||
try {
|
||||
const stats = await getUserStats(user_id)
|
||||
console.log('login_flow getUserStats success')
|
||||
const balance = await getPointsBalance(user_id)
|
||||
console.log('login_flow getPointsBalance success')
|
||||
uni.setStorageSync('user_stats', stats)
|
||||
const balance = await getPointsBalance(user_id)
|
||||
const b = balance && balance.balance !== undefined ? balance.balance : balance
|
||||
uni.setStorageSync('points_balance', b)
|
||||
} catch (e) {
|
||||
console.error('login_flow fetch stats/points error:', e && (e.message || e.errMsg))
|
||||
}
|
||||
uni.showToast({ title: '登录并绑定成功', icon: 'success' })
|
||||
console.log('login_flow navigate to index')
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
uni.setStorageSync('points_balance', b)
|
||||
} catch(e) {}
|
||||
|
||||
uni.showToast({ title: '欢迎回来', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/mine/index' }) // Redirect to Mine page
|
||||
}, 500)
|
||||
|
||||
} catch (err) {
|
||||
console.error('login_flow error:', err && (err.message || err.errMsg), 'status:', err && err.statusCode)
|
||||
error.value = err.message || '登录或绑定失败'
|
||||
error.value = err.message || '登录失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
fail: (e) => {
|
||||
console.error('login_flow uni.login fail:', e && e.errMsg)
|
||||
fail: () => {
|
||||
error.value = '微信登录失败'
|
||||
loading.value = false
|
||||
}
|
||||
@ -204,63 +212,244 @@ function onGetPhoneNumber(e) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.remember-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
.register-link {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
text-decoration: underline; /* 可选:加下划线 */
|
||||
}
|
||||
|
||||
.register-text:active {
|
||||
color: #ff6700; /* 点击时变色 */
|
||||
}
|
||||
.label {
|
||||
width: 60px; /* 固定宽度,保证对齐 */
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border: 2px solid #ccc; /* 边框加粗 */
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
font-size: 16px;
|
||||
/* 去除 iOS 默认样式 */
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
}
|
||||
.container { padding: 40rpx; display: flex; flex-direction: column; align-items: center }
|
||||
.logo { width: 200rpx; margin-top: 100rpx; margin-bottom: 40rpx }
|
||||
.title { font-size: 36rpx; margin-bottom: 20rpx }
|
||||
.btn { width: 80%; margin-top: 20rpx }
|
||||
.agreements { margin-top: 16rpx; font-size: 24rpx; color: #666; display: flex; flex-wrap: wrap; justify-content: center }
|
||||
.link { color: #007AFF; margin: 0 6rpx }
|
||||
.tip { color: #666; margin-top: 12rpx }
|
||||
.error { color: #e43; margin-top: 20rpx }
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
/* Page Container */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
background: $bg-secondary; /* Use secondary for better depth with orbs */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Orbs Background */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80rpx); /* Increased blur for smoother look */
|
||||
opacity: 0.6;
|
||||
}
|
||||
.orb-1 {
|
||||
width: 500rpx;
|
||||
height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.4), transparent 70%);
|
||||
top: -100rpx;
|
||||
left: -100rpx;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
.orb-2 {
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($accent-gold, 0.3), transparent 70%);
|
||||
bottom: -150rpx;
|
||||
right: -150rpx;
|
||||
animation: float 10s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(30rpx, 40rpx); }
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 0 40rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: $bg-glass;
|
||||
backdrop-filter: blur(30rpx);
|
||||
border-radius: 40rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.08);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Brand Section */
|
||||
.brand-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
.logo-box {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: $bg-card;
|
||||
border-radius: 40rpx;
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 12rpx 30rpx rgba($brand-primary, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: $spacing-xl;
|
||||
animation: pulse 3s infinite;
|
||||
}
|
||||
.logo { width: 100%; height: 100%; }
|
||||
.app-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 2rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
.welcome-text {
|
||||
font-size: 26rpx;
|
||||
color: $text-sub;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.input-group {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-round;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 32rpx;
|
||||
margin-bottom: $spacing-xl;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&.glass-input {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:focus-within {
|
||||
background: $bg-card;
|
||||
border-color: $brand-primary;
|
||||
box-shadow: 0 0 0 4rpx rgba($brand-primary, 0.15);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
}
|
||||
}
|
||||
.input-icon { width: 40rpx; height: 40rpx; margin-right: 20rpx; opacity: 1; }
|
||||
.input-icon image { width: 100%; height: 100%; }
|
||||
.input-field { flex: 1; height: 100%; font-size: 30rpx; color: $text-main; font-weight: 500; }
|
||||
.input-placeholder { color: $text-tertiary; font-weight: 400; }
|
||||
|
||||
.options-row { display: flex; justify-content: space-between; margin-bottom: 60rpx; padding: 0 10rpx; }
|
||||
.remember-box { display: flex; align-items: center; }
|
||||
.checkbox {
|
||||
width: 36rpx; height: 36rpx;
|
||||
border: 3rpx solid $border-color;
|
||||
border-radius: 10rpx;
|
||||
margin-right: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
background: rgba(255,255,255,0.5);
|
||||
|
||||
&.checked {
|
||||
background: $brand-primary;
|
||||
border-color: $brand-primary;
|
||||
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
|
||||
}
|
||||
}
|
||||
.check-mark { color: $text-inverse; font-size: $font-sm; font-weight: bold; }
|
||||
.remember-text { font-size: 26rpx; color: $text-sub; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
height: 96rpx;
|
||||
border-radius: $radius-round;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-lg;
|
||||
font-weight: 800;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active { transform: scale(0.96); }
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: $gradient-brand;
|
||||
color: $text-inverse;
|
||||
box-shadow: 0 10rpx 30rpx rgba($brand-primary, 0.3);
|
||||
margin-bottom: $spacing-xl;
|
||||
border: none;
|
||||
}
|
||||
.btn-shine {
|
||||
position: absolute;
|
||||
top: 0; left: -100%;
|
||||
width: 50%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transform: skewX(-20deg);
|
||||
animation: shine 3s infinite;
|
||||
}
|
||||
@keyframes shine {
|
||||
0% { left: -100%; }
|
||||
20% { left: 200%; }
|
||||
100% { left: 200%; }
|
||||
}
|
||||
|
||||
.weixin-btn {
|
||||
background: #07C160; /* WeChat Brand Color */
|
||||
color: $text-inverse;
|
||||
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
|
||||
border: none;
|
||||
}
|
||||
.wx-icon { width: 48rpx; height: 48rpx; margin-right: 16rpx; filter: brightness(100); } /* Make logo white if it's the logo, but ideally it should be WeChat icon */
|
||||
|
||||
/* Register Link */
|
||||
.register-link { text-align: center; margin-top: $spacing-xl; }
|
||||
.register-text { font-size: $font-md; color: $text-sub; }
|
||||
.highlight { color: $brand-primary; font-weight: 700; margin-left: 8rpx; }
|
||||
|
||||
/* Agreements */
|
||||
.agreements {
|
||||
margin-top: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
.checkbox.round { border-radius: 50%; width: 32rpx; height: 32rpx; margin-top: 4rpx; }
|
||||
.checkbox-area { padding-right: 12rpx; }
|
||||
.agreement-text {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
}
|
||||
.link { color: $brand-primary; text-decoration: none; font-weight: 600; margin: 0 4rpx; }
|
||||
|
||||
.error-toast {
|
||||
position: fixed;
|
||||
top: 100rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba($uni-color-error, 0.9);
|
||||
color: $text-inverse;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
z-index: 999;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.2);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from { transform: translate(-50%, -100%); opacity: 0; }
|
||||
to { transform: translate(-50%, 0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
1960
pages/mine/index.vue
1960
pages/mine/index.vue
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<view class="wrap">
|
||||
<view class="bg-decoration"></view>
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'pending' }" @click="switchTab('pending')">待付款</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'completed' }" @click="switchTab('completed')">已完成</view>
|
||||
</view>
|
||||
<view v-if="error" class="error">{{ error }}</view>
|
||||
<view v-if="orders.length === 0 && !loading" class="empty">暂无订单</view>
|
||||
<view v-if="orders.length === 0 && !loading" class="empty">
|
||||
<view class="empty-icon">📦</view>
|
||||
<view class="empty-text">暂无订单</view>
|
||||
</view>
|
||||
<view v-for="item in orders" :key="item.id || item.order_no" class="order">
|
||||
<view class="order-main">
|
||||
<view class="order-title">{{ item.title || item.subject || '订单' }}</view>
|
||||
@ -13,7 +17,7 @@
|
||||
</view>
|
||||
<view class="order-right">
|
||||
<view class="order-amount">{{ formatAmount(item.total_amount || item.amount || item.price) }}</view>
|
||||
<view class="order-status">{{ statusText(item) }}</view>
|
||||
<view class="order-status" :class="getStatusClass(item)">{{ statusText(item) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="loading">加载中...</view>
|
||||
@ -65,6 +69,13 @@ function statusText(item) {
|
||||
return s || ''
|
||||
}
|
||||
|
||||
function getStatusClass(item) {
|
||||
const text = statusText(item)
|
||||
if (text === '待付款') return 'status-pending'
|
||||
if (text === '已完成') return 'status-completed'
|
||||
return ''
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
if (currentTab.value === tab) return
|
||||
currentTab.value = tab
|
||||
@ -163,20 +174,175 @@ onReachBottom(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap { padding: 24rpx }
|
||||
.tabs { display: flex; background: #fff; border-radius: 12rpx; padding: 8rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
|
||||
.tab { flex: 1; text-align: center; padding: 16rpx 0; font-size: 28rpx; color: #666 }
|
||||
.tab.active { color: #007AFF; font-weight: 600 }
|
||||
.order { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
|
||||
.order-main { display: flex; flex-direction: column }
|
||||
.order-title { font-size: 28rpx; color: #333 }
|
||||
.order-sub { font-size: 24rpx; color: #999; margin-top: 6rpx }
|
||||
.order-right { display: flex; flex-direction: column; align-items: flex-end }
|
||||
.order-amount { font-size: 28rpx; color: #333 }
|
||||
.order-status { font-size: 24rpx; color: #666; margin-top: 6rpx }
|
||||
.empty { text-align: center; color: #999; margin-top: 40rpx }
|
||||
.error { color: #e43; margin-bottom: 12rpx }
|
||||
.loading { text-align: center; color: #666; margin: 20rpx 0 }
|
||||
.end { text-align: center; color: #999; margin: 20rpx 0 }
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 订单页面
|
||||
采用暖橙色调的订单列表设计
|
||||
============================================ */
|
||||
|
||||
.wrap {
|
||||
padding: $spacing-md;
|
||||
min-height: 100vh;
|
||||
background-color: $bg-page;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100vh;
|
||||
background: radial-gradient(circle at 10% 10%, rgba($brand-primary, 0.05), transparent 40%),
|
||||
radial-gradient(circle at 90% 90%, rgba($accent-gold, 0.05), transparent 40%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tab 切换 */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: $bg-glass;
|
||||
backdrop-filter: blur(10rpx);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: $radius-lg;
|
||||
padding: 8rpx;
|
||||
margin-bottom: $spacing-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
font-size: $font-md;
|
||||
color: $text-sub;
|
||||
border-radius: $radius-md;
|
||||
transition: all 0.25s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tab.active {
|
||||
background: $gradient-brand;
|
||||
color: $text-inverse;
|
||||
font-weight: 600;
|
||||
box-shadow: $shadow-glow;
|
||||
}
|
||||
|
||||
/* 订单卡片 */
|
||||
.order {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
border: 1rpx solid rgba(0,0,0,0.02);
|
||||
transition: all 0.2s ease;
|
||||
animation: fadeInUp 0.4s ease-out backwards;
|
||||
}
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
.order:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
}
|
||||
}
|
||||
|
||||
.order:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.order-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.order-title {
|
||||
font-size: $font-md;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
.order-sub {
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.order-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-left: $spacing-lg;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.order-amount {
|
||||
font-size: $font-lg;
|
||||
font-weight: 800;
|
||||
color: $brand-primary;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
}
|
||||
.order-status {
|
||||
font-size: $font-xs;
|
||||
color: $text-sub;
|
||||
margin-top: 10rpx;
|
||||
padding: 4rpx $spacing-md;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 600;
|
||||
|
||||
&.status-pending {
|
||||
background: rgba($brand-primary, 0.1);
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: rgba($uni-color-success, 0.1);
|
||||
color: $uni-color-success;
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 120rpx;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: $spacing-lg;
|
||||
opacity: 0.6;
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: $text-sub;
|
||||
font-size: $font-md;
|
||||
}
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error {
|
||||
color: $uni-color-error;
|
||||
font-size: $font-sm;
|
||||
margin-bottom: $spacing-md;
|
||||
padding: $spacing-md;
|
||||
background: rgba($uni-color-error, 0.1);
|
||||
border-radius: $radius-lg;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading, .end {
|
||||
text-align: center;
|
||||
color: $text-sub;
|
||||
padding: $spacing-lg 0;
|
||||
font-size: $font-sm;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,18 +1,55 @@
|
||||
<template>
|
||||
<view class="wrap">
|
||||
<view v-if="error" class="error">{{ error }}</view>
|
||||
<view v-if="records.length === 0 && !loading" class="empty">暂无积分记录</view>
|
||||
<view v-for="item in records" :key="item.id || item.time || item.created_at" class="record">
|
||||
<view class="record-main">
|
||||
<view class="record-title">{{ item.title || item.reason || '变更' }}</view>
|
||||
<view class="record-time">{{ formatTime(item.time || item.created_at) }}</view>
|
||||
</view>
|
||||
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
|
||||
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
|
||||
</view>
|
||||
<!-- 顶部装饰背景 -->
|
||||
<view class="page-bg-decoration"></view>
|
||||
|
||||
<view class="header-area">
|
||||
<view class="page-title">积分明细</view>
|
||||
<view class="page-subtitle">Points Record</view>
|
||||
</view>
|
||||
|
||||
<view class="content-area">
|
||||
<view v-if="error" class="error-card">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text>{{ error }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="records.length === 0 && !loading" class="empty-state">
|
||||
<image class="empty-img" src="/static/empty-points.png" mode="widthFix" />
|
||||
<text class="empty-text">暂无积分记录</text>
|
||||
</view>
|
||||
|
||||
<view class="records-list" v-else>
|
||||
<view
|
||||
v-for="(item, index) in records"
|
||||
:key="item.id || item.time || item.created_at"
|
||||
class="record-item"
|
||||
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||
>
|
||||
<view class="record-icon" :class="{ 'is-add': (item.change || item.amount || 0) > 0 }">
|
||||
{{ (item.change || item.amount || 0) > 0 ? '↓' : '↑' }}
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<view class="record-main">
|
||||
<view class="record-title">{{ item.title || item.reason || '积分变更' }}</view>
|
||||
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
|
||||
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-footer">
|
||||
<view class="record-time">{{ formatTime(item.time || item.created_at) }}</view>
|
||||
<view class="record-status">交易成功</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingMore" class="loading-more">
|
||||
<view class="spinner"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && records.length > 0" class="no-more">- 到底啦 -</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="loading">加载中...</view>
|
||||
<view v-else-if="!hasMore && records.length > 0" class="end">没有更多了</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -101,17 +138,202 @@ onReachBottom(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap { padding: 24rpx }
|
||||
.record { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
|
||||
.record-main { display: flex; flex-direction: column }
|
||||
.record-title { font-size: 28rpx; color: #333 }
|
||||
.record-time { font-size: 24rpx; color: #999; margin-top: 6rpx }
|
||||
.record-amount { font-size: 32rpx }
|
||||
.record-amount.inc { color: #18a058 }
|
||||
.record-amount.dec { color: #d03050 }
|
||||
.empty { text-align: center; color: #999; margin-top: 40rpx }
|
||||
.error { color: #e43; margin-bottom: 12rpx }
|
||||
.loading { text-align: center; color: #666; margin: 20rpx 0 }
|
||||
.end { text-align: center; color: #999; margin: 20rpx 0 }
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-page;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.page-bg-decoration {
|
||||
position: absolute;
|
||||
top: -200rpx;
|
||||
right: -200rpx;
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.15), transparent 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.header-area {
|
||||
padding: $spacing-xl $spacing-lg;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.page-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: $text-tertiary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 0 $spacing-lg $spacing-xl;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba($bg-card, 0.8);
|
||||
border-radius: $radius-lg;
|
||||
padding: 30rpx;
|
||||
box-shadow: $shadow-sm;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1rpx solid rgba(255,255,255,0.5);
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background: rgba($bg-card, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.record-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-secondary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: $text-secondary;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
font-weight: 800;
|
||||
|
||||
&.is-add {
|
||||
background: rgba($uni-color-success, 0.1);
|
||||
color: $uni-color-success;
|
||||
}
|
||||
}
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.record-amount {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
|
||||
&.inc { color: $uni-color-success; }
|
||||
&.dec { color: $text-main; }
|
||||
}
|
||||
|
||||
.record-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 24rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
background: $bg-secondary;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
|
||||
.empty-img {
|
||||
width: 240rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.6;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
.empty-text {
|
||||
color: $text-tertiary;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: rgba($uni-color-error, 0.05);
|
||||
border: 1rpx solid rgba($uni-color-error, 0.1);
|
||||
color: $uni-color-error;
|
||||
padding: 20rpx;
|
||||
border-radius: $radius-md;
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
|
||||
.error-icon {
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx 0;
|
||||
color: $text-tertiary;
|
||||
font-size: 24rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
border: 3rpx solid $bg-secondary;
|
||||
border-top-color: $text-tertiary;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: $text-tertiary;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
156
pages/register/register.vue
Normal file
156
pages/register/register.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
||||
<view class="title">注册新账号</view>
|
||||
|
||||
<view class="form">
|
||||
<view class="input-row">
|
||||
<text class="label">账号</text>
|
||||
<input type="text" v-model="account" class="input-field" placeholder="请输入账号" />
|
||||
</view>
|
||||
|
||||
<view class="input-row">
|
||||
<text class="label">密码</text>
|
||||
<input type="password" v-model="password" class="input-field" placeholder="请输入密码" />
|
||||
</view>
|
||||
|
||||
<view class="input-row">
|
||||
<text class="label">确认密码</text>
|
||||
<input type="password" v-model="confirmPassword" class="input-field" placeholder="请再次输入密码" />
|
||||
</view>
|
||||
|
||||
<button class="btn submit-btn" :disabled="loading" @click="onRegister">注册</button>
|
||||
</view>
|
||||
|
||||
<view class="login-link">
|
||||
<text @tap="goLogin">已有账号?去登录</text>
|
||||
</view>
|
||||
|
||||
<view v-if="error" class="error">{{ error }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const account = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
function goLogin() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function onRegister() {
|
||||
if (!account.value || !password.value) {
|
||||
uni.showToast({ title: '请填写完整', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// TODO: 调用注册 API
|
||||
uni.showToast({ title: '注册功能开发中', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 注册页面
|
||||
============================================ */
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 60rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, #FFF8F3 0%, #FFE8D1 50%, #FFDAB9 100%);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-top: 80rpx;
|
||||
margin-bottom: 32rpx;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 12rpx 36rpx rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1F2937;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
background: #F9FAFB;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 140rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 28rpx;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
margin-top: 32rpx;
|
||||
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
|
||||
color: #FFFFFF !important;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
|
||||
}
|
||||
.submit-btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.login-link {
|
||||
margin-top: 32rpx;
|
||||
font-size: 26rpx;
|
||||
color: #FF9F43;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 24rpx;
|
||||
color: #EF4444;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
163
pages/shop/detail.vue
Normal file
163
pages/shop/detail.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="bg-decoration"></view>
|
||||
<view class="loading" v-if="loading">加载中...</view>
|
||||
<view v-else-if="detail.id" class="detail-wrap">
|
||||
<image v-if="detail.main_image" class="main-image" :src="detail.main_image" mode="widthFix" />
|
||||
<view class="info-card">
|
||||
<view class="title">{{ detail.title || detail.name || '-' }}</view>
|
||||
<view class="price-row">
|
||||
<text class="price">¥{{ formatPrice(detail.price_sale || detail.price) }}</text>
|
||||
<text class="points" v-if="detail.points_required">{{ detail.points_required }}积分</text>
|
||||
</view>
|
||||
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存:{{ detail.stock }}</view>
|
||||
<view class="desc" v-if="detail.description">{{ detail.description }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">商品不存在</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getProductDetail } from '../../api/appUser'
|
||||
|
||||
const detail = ref({})
|
||||
const loading = ref(false)
|
||||
|
||||
function formatPrice(p) {
|
||||
if (p === undefined || p === null) return '0.00'
|
||||
return (Number(p) / 100).toFixed(2)
|
||||
}
|
||||
|
||||
async function fetchDetail(id) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductDetail(id)
|
||||
detail.value = res || {}
|
||||
} catch (e) {
|
||||
detail.value = {}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
const id = opts && opts.id
|
||||
if (id) fetchDetail(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
奇盒潮玩 - 商品详情页
|
||||
============================================ */
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
text-align: center;
|
||||
padding: 120rpx 40rpx;
|
||||
color: $text-secondary;
|
||||
font-size: $font-md;
|
||||
}
|
||||
|
||||
.detail-wrap {
|
||||
padding-bottom: 40rpx;
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
.main-image {
|
||||
width: 100%;
|
||||
height: 750rpx; /* Square aspect ratio */
|
||||
display: block;
|
||||
background: $bg-secondary;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin: $spacing-lg;
|
||||
margin-top: -60rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: $radius-xl;
|
||||
padding: $spacing-xl;
|
||||
box-shadow: $shadow-lg;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-xl;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
margin-bottom: $spacing-md;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: $font-xxl;
|
||||
font-weight: 900;
|
||||
color: $brand-primary;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: $font-md;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.points {
|
||||
font-size: $font-sm;
|
||||
color: $brand-primary;
|
||||
padding: 6rpx $spacing-md;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
border-radius: 100rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock {
|
||||
font-size: $font-sm;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $spacing-lg;
|
||||
background: $bg-secondary;
|
||||
display: inline-block;
|
||||
padding: 6rpx $spacing-md;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: $font-lg;
|
||||
color: $text-main;
|
||||
line-height: 1.8;
|
||||
padding-top: $spacing-lg;
|
||||
border-top: 1rpx dashed $border-color-light;
|
||||
|
||||
&::before {
|
||||
content: '商品详情';
|
||||
display: block;
|
||||
font-size: $font-md;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $spacing-sm;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(40rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@ -1,52 +1,60 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="showNotice" class="notice-mask" @touchmove.stop.prevent @tap.stop>
|
||||
<view class="notice-dialog" @tap.stop>
|
||||
<view class="notice-title">提示</view>
|
||||
<view class="notice-content">由于价格浮动,当前暂不支持自行兑换商品,兑换请联系客服核对价格。</view>
|
||||
<view class="notice-actions">
|
||||
<view class="notice-check" @tap.stop="toggleHideForever">
|
||||
<view class="check-box" :class="{ on: hideForever }"></view>
|
||||
<text class="check-text">不再显示</text>
|
||||
</view>
|
||||
<button class="notice-btn" type="primary" @tap.stop="onDismissNotice">我知道了</button>
|
||||
<view class="bg-decoration"></view>
|
||||
<!-- 顶部固定区域 -->
|
||||
<view class="header-section">
|
||||
<view class="search-box">
|
||||
<view class="search-input-wrap">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input class="search-input" v-model="keyword" placeholder="搜索心仪的商品" placeholder-class="placeholder-style" confirm-type="search" @confirm="onSearchConfirm" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-row">
|
||||
<view class="price-range">
|
||||
<text class="price-label">价格区间</text>
|
||||
<input class="price-input" type="number" v-model="minPrice" placeholder="最低" placeholder-class="price-ph" />
|
||||
<text class="price-sep">-</text>
|
||||
<input class="price-input" type="number" v-model="maxPrice" placeholder="最高" placeholder-class="price-ph" />
|
||||
</view>
|
||||
<button class="filter-btn" hover-class="btn-hover" @tap="onApplyFilters">筛选</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
|
||||
<view class="products-section" v-else>
|
||||
<view class="section-title">商品</view>
|
||||
<view class="toolbar">
|
||||
<input class="search" v-model="keyword" placeholder="搜索商品" confirm-type="search" @confirm="onSearchConfirm" />
|
||||
<view class="filters">
|
||||
<input class="price" type="number" v-model="minPrice" placeholder="最低价" />
|
||||
<text class="dash">-</text>
|
||||
<input class="price" type="number" v-model="maxPrice" placeholder="最高价" />
|
||||
<button class="apply-btn" size="mini" @tap="onApplyFilters">筛选</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="displayCount" class="products-columns">
|
||||
<view class="column" v-for="(col, ci) in columns" :key="ci">
|
||||
<view class="product-item" v-for="p in col" :key="p.id" @tap="onProductTap(p)">
|
||||
|
||||
<!-- 占位,防止内容被头部遮挡 -->
|
||||
<view class="header-placeholder"></view>
|
||||
|
||||
<view v-if="loading && !products.length" class="loading-wrap"><view class="spinner"></view></view>
|
||||
|
||||
<view class="products-container" v-else>
|
||||
<view v-if="products.length > 0" class="products-grid">
|
||||
<view class="product-item" v-for="p in products" :key="p.id" @tap="onProductTap(p)">
|
||||
<view class="product-card">
|
||||
<view class="thumb-wrap">
|
||||
<image class="product-thumb" :class="{ visible: isLoaded(p) }" :src="p.image" mode="widthFix" lazy-load="true" @load="onImageLoad(p)" @error="onImageError(p)" />
|
||||
<view v-if="!isLoaded(p)" class="skeleton"></view>
|
||||
<view class="badge">
|
||||
<text class="badge-price" v-if="p.price !== null">¥{{ p.price }}</text>
|
||||
<text class="badge-points" v-if="p.points !== null">{{ p.points }}积分</text>
|
||||
</view>
|
||||
<image class="product-thumb" :src="p.image" mode="aspectFill" lazy-load="true" />
|
||||
<view class="stock-tag" v-if="p.stock !== null && p.stock < 10">仅剩{{p.stock}}件</view>
|
||||
</view>
|
||||
<text class="product-title">{{ p.title }}</text>
|
||||
<view class="product-extra" v-if="p.stock !== null">
|
||||
<text class="stock">库存 {{ p.stock }}</text>
|
||||
<view class="product-info">
|
||||
<text class="product-title">{{ p.title }}</text>
|
||||
<view class="product-bottom">
|
||||
<view class="price-row">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-val">{{ p.price }}</text>
|
||||
</view>
|
||||
<view class="points-badge" v-if="p.points">
|
||||
<text class="points-val">{{ p.points }}</text>
|
||||
<text class="points-unit">积分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||||
<text class="empty-text">暂无相关商品</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">暂无商品</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -56,98 +64,29 @@ import { ref, computed } from 'vue'
|
||||
import { request, authRequest } from '../../utils/request.js'
|
||||
|
||||
const products = ref([])
|
||||
const columns = ref([[], []])
|
||||
const colHeights = ref([0, 0])
|
||||
const CACHE_KEY = 'products_cache_v1'
|
||||
const TTL_MS = 10 * 60 * 1000
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const minPrice = ref('')
|
||||
const maxPrice = ref('')
|
||||
const displayCount = computed(() => (columns.value[0].length + columns.value[1].length))
|
||||
const loadedMap = ref({})
|
||||
const showNotice = ref(false)
|
||||
const hideForever = ref(false)
|
||||
const skipReloadOnce = ref(false)
|
||||
function getKey(p) { return String((p && p.id) ?? '') + '|' + String((p && p.image) ?? '') }
|
||||
function unwrap(list) {
|
||||
if (Array.isArray(list)) return list
|
||||
const obj = list || {}
|
||||
const data = obj.data || {}
|
||||
const arr = obj.list || obj.items || data.list || data.items || data
|
||||
return Array.isArray(arr) ? arr : []
|
||||
}
|
||||
function cleanUrl(u) {
|
||||
const s = String(u || '').trim()
|
||||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||||
if (m && m[0]) return m[0]
|
||||
return s.replace(/[`'\"]/g, '').trim()
|
||||
}
|
||||
|
||||
function apiGet(url, data = {}) {
|
||||
const token = uni.getStorageSync('token')
|
||||
const fn = token ? authRequest : request
|
||||
return fn({ url, method: 'GET', data })
|
||||
}
|
||||
|
||||
function getCachedProducts() {
|
||||
try {
|
||||
const obj = uni.getStorageSync(CACHE_KEY)
|
||||
if (obj && Array.isArray(obj.data) && typeof obj.ts === 'number') {
|
||||
const fresh = Date.now() - obj.ts < TTL_MS
|
||||
if (fresh) return obj.data
|
||||
}
|
||||
} catch (_) {}
|
||||
return null
|
||||
}
|
||||
|
||||
function setCachedProducts(list) {
|
||||
try {
|
||||
uni.setStorageSync(CACHE_KEY, { data: Array.isArray(list) ? list : [], ts: Date.now() })
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function estimateHeight(p) {
|
||||
const base = 220
|
||||
const len = String(p.title || '').length
|
||||
const lines = Math.min(2, Math.ceil(len / 12))
|
||||
const titleH = lines * 36
|
||||
const stockH = p.stock !== null && p.stock !== undefined ? 34 : 0
|
||||
const padding = 28
|
||||
return base + titleH + stockH + padding
|
||||
}
|
||||
|
||||
function distributeToColumns(list) {
|
||||
const arr = Array.isArray(list) ? list : []
|
||||
const cols = Array.from({ length: 2 }, () => [])
|
||||
const hs = [0, 0]
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const h = estimateHeight(arr[i])
|
||||
const idx = hs[0] <= hs[1] ? 0 : 1
|
||||
cols[idx].push(arr[i])
|
||||
hs[idx] += h
|
||||
}
|
||||
columns.value = cols
|
||||
colHeights.value = hs
|
||||
const presentKeys = new Set(arr.map(getKey))
|
||||
const next = {}
|
||||
const prev = loadedMap.value || {}
|
||||
for (const k in prev) { if (presentKeys.has(k)) next[k] = prev[k] }
|
||||
loadedMap.value = next
|
||||
}
|
||||
|
||||
function extractListAndTotal(payload) {
|
||||
if (Array.isArray(payload)) return { list: payload, total: payload.length }
|
||||
const obj = payload || {}
|
||||
const data = obj.data || {}
|
||||
const list = obj.list || obj.items || data.list || data.items || []
|
||||
const totalRaw = obj.total ?? data.total
|
||||
const total = typeof totalRaw === 'number' ? totalRaw : (Array.isArray(list) ? list.length : 0)
|
||||
return { list: Array.isArray(list) ? list : [], total }
|
||||
function cleanUrl(u) {
|
||||
const s = String(u || '').trim()
|
||||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||||
if (m && m[0]) return m[0]
|
||||
return s.replace(/[`'\"]/g, '').trim()
|
||||
}
|
||||
|
||||
function normalizeProducts(list) {
|
||||
const arr = unwrap(list)
|
||||
return arr.map((i, idx) => ({
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((i, idx) => ({
|
||||
id: i.id ?? i.productId ?? i._id ?? i.sku_id ?? String(idx),
|
||||
image: cleanUrl(i.main_image ?? i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
|
||||
title: i.title ?? i.name ?? i.product_name ?? i.sku_name ?? '',
|
||||
@ -169,14 +108,18 @@ function onProductTap(p) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter logic
|
||||
const allProducts = ref([]) // Store all fetched products for client-side filtering
|
||||
|
||||
function applyFilters() {
|
||||
const k = String(keyword.value || '').trim().toLowerCase()
|
||||
const min = Number(minPrice.value)
|
||||
const max = Number(maxPrice.value)
|
||||
const hasMin = !isNaN(min) && String(minPrice.value).trim() !== ''
|
||||
const hasMax = !isNaN(max) && String(maxPrice.value).trim() !== ''
|
||||
const list = Array.isArray(products.value) ? products.value : []
|
||||
const filtered = list.filter(p => {
|
||||
|
||||
const list = allProducts.value
|
||||
products.value = list.filter(p => {
|
||||
const title = String(p.title || '').toLowerCase()
|
||||
if (k && !title.includes(k)) return false
|
||||
const priceNum = typeof p.price === 'number' ? p.price : Number(p.price)
|
||||
@ -190,7 +133,6 @@ function applyFilters() {
|
||||
}
|
||||
return true
|
||||
})
|
||||
distributeToColumns(filtered)
|
||||
}
|
||||
|
||||
function onSearchConfirm() { applyFilters() }
|
||||
@ -198,56 +140,50 @@ function onApplyFilters() { applyFilters() }
|
||||
|
||||
async function loadProducts() {
|
||||
try {
|
||||
const cached = getCachedProducts()
|
||||
if (cached) {
|
||||
products.value = cached
|
||||
distributeToColumns(cached)
|
||||
return
|
||||
const cached = uni.getStorageSync(CACHE_KEY)
|
||||
if (cached && cached.data && Date.now() - cached.ts < TTL_MS) {
|
||||
allProducts.value = cached.data
|
||||
applyFilters()
|
||||
return
|
||||
}
|
||||
|
||||
const first = await apiGet('/api/app/products', { page: 1 })
|
||||
const { list: firstList, total } = extractListAndTotal(first)
|
||||
// Simple extraction
|
||||
let list = []
|
||||
let total = 0
|
||||
if (first && first.list) { list = first.list; total = first.total }
|
||||
else if (first && first.data && first.data.list) { list = first.data.list; total = first.data.total }
|
||||
|
||||
// If not too many, fetch all for better client UX
|
||||
const pageSize = 20
|
||||
const totalPages = Math.max(1, Math.ceil(((typeof total === 'number' ? total : 0)) / pageSize))
|
||||
if (totalPages <= 1) {
|
||||
const normalized = normalizeProducts(firstList)
|
||||
products.value = normalized
|
||||
distributeToColumns(normalized)
|
||||
setCachedProducts(normalized)
|
||||
return
|
||||
const totalPages = Math.ceil((total || 0) / pageSize)
|
||||
|
||||
if (totalPages > 1) {
|
||||
const tasks = []
|
||||
for (let p = 2; p <= totalPages; p++) {
|
||||
tasks.push(apiGet('/api/app/products', { page: p }))
|
||||
}
|
||||
const results = await Promise.allSettled(tasks)
|
||||
results.forEach(r => {
|
||||
if (r.status === 'fulfilled') {
|
||||
const val = r.value
|
||||
const subList = (val && val.list) || (val && val.data && val.data.list) || []
|
||||
if (Array.isArray(subList)) list = list.concat(subList)
|
||||
}
|
||||
})
|
||||
}
|
||||
const tasks = []
|
||||
for (let p = 2; p <= totalPages; p++) {
|
||||
tasks.push(apiGet('/api/app/products', { page: p }))
|
||||
}
|
||||
const results = await Promise.allSettled(tasks)
|
||||
const restLists = results.map(r => {
|
||||
if (r.status === 'fulfilled') {
|
||||
const { list } = extractListAndTotal(r.value)
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
return []
|
||||
})
|
||||
const merged = [firstList, ...restLists].flat()
|
||||
const normalized = normalizeProducts(merged)
|
||||
products.value = normalized
|
||||
distributeToColumns(normalized)
|
||||
setCachedProducts(normalized)
|
||||
|
||||
const normalized = normalizeProducts(list)
|
||||
allProducts.value = normalized
|
||||
applyFilters()
|
||||
uni.setStorageSync(CACHE_KEY, { data: normalized, ts: Date.now() })
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
products.value = []
|
||||
columns.value = [[], []]
|
||||
colHeights.value = [0, 0]
|
||||
const presentKeys = new Set([])
|
||||
const next = {}
|
||||
const prev = loadedMap.value || {}
|
||||
for (const k in prev) { if (presentKeys.has(k)) next[k] = prev[k] }
|
||||
loadedMap.value = next
|
||||
}
|
||||
}
|
||||
|
||||
function isLoaded(p) { return !!(loadedMap.value && loadedMap.value[getKey(p)]) }
|
||||
function onImageLoad(p) { const k = getKey(p); if (!k) return; loadedMap.value = { ...(loadedMap.value || {}), [k]: true } }
|
||||
function onImageError(p) { const k = getKey(p); if (!k) return; const prev = { ...(loadedMap.value || {}) }; delete prev[k]; loadedMap.value = prev }
|
||||
|
||||
onShow(async () => {
|
||||
const token = uni.getStorageSync('token')
|
||||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||||
@ -264,77 +200,353 @@ onShow(async () => {
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const sess = String(uni.getStorageSync('app_session_id') || '')
|
||||
const hiddenSess = String(uni.getStorageSync('shop_notice_hidden_session_id') || '')
|
||||
const hiddenThisSession = !!(sess && hiddenSess && hiddenSess === sess)
|
||||
showNotice.value = !hiddenThisSession
|
||||
hideForever.value = hiddenThisSession
|
||||
} catch (_) { showNotice.value = true; hideForever.value = false }
|
||||
try {
|
||||
const skip = !!uni.getStorageSync('shop_skip_reload_once')
|
||||
if (skipReloadOnce.value || skip) {
|
||||
skipReloadOnce.value = false
|
||||
uni.setStorageSync('shop_skip_reload_once', '')
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
loading.value = true
|
||||
await loadProducts()
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
function toggleHideForever() { hideForever.value = !hideForever.value }
|
||||
function onDismissNotice() {
|
||||
if (hideForever.value) {
|
||||
try {
|
||||
const sess = String(uni.getStorageSync('app_session_id') || '')
|
||||
if (sess) uni.setStorageSync('shop_notice_hidden_session_id', sess)
|
||||
} catch (_) {}
|
||||
// 分享功能
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: '奇盒潮玩商城 - 好物等你来兑',
|
||||
path: `/pages/index/index?invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
showNotice.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onShareTimeline(() => {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: '奇盒潮玩商城 - 好物等你来兑',
|
||||
query: `invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 24rpx }
|
||||
.notice-mask { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); z-index: 9999; display: flex; align-items: center; justify-content: center }
|
||||
.notice-dialog { width: 86%; max-width: 640rpx; background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 12rpx 24rpx rgba(0,0,0,0.18) }
|
||||
.notice-title { font-size: 32rpx; font-weight: 600; padding: 24rpx 24rpx 0 }
|
||||
.notice-content { padding: 16rpx 24rpx; font-size: 26rpx; color: #333; line-height: 1.6 }
|
||||
.notice-actions { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx 24rpx }
|
||||
.notice-check { display: flex; align-items: center; gap: 8rpx }
|
||||
.check-box { width: 28rpx; height: 28rpx; border-radius: 6rpx; border: 2rpx solid #007AFF; background: #fff }
|
||||
.check-box.on { background: #007AFF }
|
||||
.check-text { font-size: 26rpx; color: #555 }
|
||||
.notice-btn { background: #007AFF; color: #fff; border-radius: 999rpx; padding: 0 28rpx }
|
||||
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
|
||||
.products-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx; margin-top: 24rpx }
|
||||
.loading-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center }
|
||||
.spinner { width: 56rpx; height: 56rpx; border: 6rpx solid rgba(0,122,255,0.15); border-top-color: #007AFF; border-radius: 50%; animation: spin 1s linear infinite }
|
||||
@keyframes spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
|
||||
.toolbar { display: flex; flex-direction: column; gap: 12rpx; margin-bottom: 16rpx }
|
||||
.search { background: #f6f8ff; border: 1rpx solid rgba(0,122,255,0.25); border-radius: 999rpx; padding: 14rpx 20rpx; font-size: 26rpx }
|
||||
.filters { display: flex; align-items: center; gap: 12rpx }
|
||||
.price { flex: 1; background: #f6f8ff; border: 1rpx solid rgba(0,122,255,0.25); border-radius: 999rpx; padding: 12rpx 16rpx; font-size: 26rpx }
|
||||
.dash { color: #888; font-size: 26rpx }
|
||||
.apply-btn { background: #007AFF; color: #fff; border-radius: 999rpx; padding: 0 20rpx }
|
||||
.products-columns { display: flex; gap: 12rpx }
|
||||
.column { flex: 1 }
|
||||
.product-item { margin-bottom: 12rpx }
|
||||
.empty { padding: 40rpx; color: #888; text-align: center }
|
||||
.product-card { background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,122,255,0.08); transition: transform .15s ease }
|
||||
.product-item:active .product-card { transform: scale(0.98) }
|
||||
.thumb-wrap { position: relative }
|
||||
.product-thumb { width: 100%; height: auto; display: block; opacity: 0; transition: opacity .25s ease; z-index: 0 }
|
||||
.product-thumb.visible { opacity: 1 }
|
||||
.skeleton { position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: linear-gradient(90deg, #eef2ff 25%, #f6f8ff 37%, #eef2ff 63%); background-size: 400% 100%; animation: shimmer 1.2s ease infinite; z-index: 1 }
|
||||
@keyframes shimmer { 0% { background-position: 100% 0 } 100% { background-position: 0 0 } }
|
||||
.thumb-wrap { background: #f6f8ff; min-height: 220rpx }
|
||||
.badge { position: absolute; left: 12rpx; bottom: 12rpx; display: flex; gap: 8rpx }
|
||||
.badge-price { background: #007AFF; color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; box-shadow: 0 2rpx 8rpx rgba(0,122,255,0.25) }
|
||||
.badge-points { background: rgba(0,122,255,0.85); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx }
|
||||
.product-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin: 12rpx; font-size: 26rpx; color: #222 }
|
||||
.product-extra { display: flex; justify-content: flex-end; align-items: center; margin: 0 12rpx 12rpx }
|
||||
.stock { font-size: 22rpx; color: #888 }
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-page;
|
||||
padding-bottom: 40rpx;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -100rpx; left: -100rpx;
|
||||
width: 600rpx; height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.1) 0%, transparent 70%);
|
||||
filter: blur(60rpx);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 10%; right: -100rpx;
|
||||
width: 500rpx; height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
||||
filter: blur(50rpx);
|
||||
border-radius: 50%;
|
||||
opacity: 0.5;
|
||||
animation: float 15s ease-in-out infinite reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(30rpx, 50rpx); }
|
||||
}
|
||||
|
||||
/* 顶部 Header */
|
||||
.header-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: $bg-glass;
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 0 24rpx 24rpx;
|
||||
box-shadow: $shadow-sm;
|
||||
border-bottom: 1rpx solid $border-color-light;
|
||||
}
|
||||
.header-placeholder {
|
||||
height: 160rpx; /* 根据 header 高度调整 */
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.search-box {
|
||||
margin-bottom: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.search-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: $radius-round;
|
||||
padding: 18rpx 24rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.search-input-wrap:focus-within {
|
||||
background: $bg-card;
|
||||
border-color: $brand-primary;
|
||||
box-shadow: 0 0 0 4rpx rgba($brand-primary, 0.1);
|
||||
}
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 16rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: $text-main;
|
||||
}
|
||||
.placeholder-style {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 筛选行 */
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.price-range {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $bg-secondary;
|
||||
border-radius: $radius-md;
|
||||
padding: 10rpx 20rpx;
|
||||
}
|
||||
.price-label {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.price-input {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
color: $text-main;
|
||||
}
|
||||
.price-ph {
|
||||
color: $text-tertiary;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.price-sep {
|
||||
color: $text-tertiary;
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
.filter-btn {
|
||||
background: $gradient-brand;
|
||||
color: $text-inverse;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
border-radius: $radius-md;
|
||||
padding: 0 32rpx;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
border: none;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
.btn-hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 商品 Grid 容器 */
|
||||
.products-container {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 商品卡片 */
|
||||
.product-item {
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
}
|
||||
@for $i from 1 through 10 {
|
||||
.product-item:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
}
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.product-item:active .product-card {
|
||||
transform: scale(0.98);
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.thumb-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%; /* 1:1 Aspect Ratio */
|
||||
background: $bg-secondary;
|
||||
}
|
||||
.product-thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.stock-tag {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba($accent-red, 0.9);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-top-left-radius: 12rpx;
|
||||
backdrop-filter: blur(4px);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 -2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 20rpx;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
font-size: 28rpx;
|
||||
color: $text-main;
|
||||
line-height: 1.4;
|
||||
height: 2.8em; /* 2 lines */
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.price-row {
|
||||
color: $accent-red;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.price-symbol {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.price-val {
|
||||
font-size: 34rpx;
|
||||
}
|
||||
.points-badge {
|
||||
background: rgba($brand-primary, 0.1);
|
||||
color: $brand-primary;
|
||||
border: 1px solid rgba($brand-primary, 0.2);
|
||||
border-radius: 8rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.points-val {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.points-unit {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
/* Loading & Empty */
|
||||
.loading-wrap {
|
||||
padding: 100rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border: 4rpx solid rgba($brand-primary, 0.2);
|
||||
border-top-color: $brand-primary;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 100rpx;
|
||||
}
|
||||
.empty-img {
|
||||
width: 240rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.empty-text {
|
||||
color: $text-tertiary;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.check-text {
|
||||
font-size: 26rpx;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
25
project.config.json
Normal file
25
project.config.json
Normal 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": {}
|
||||
}
|
||||
14
project.private.config.json
Normal file
14
project.private.config.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"libVersion": "3.12.1",
|
||||
"projectname": "bindbox-mini",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
211
uni.scss
211
uni.scss
@ -1,76 +1,185 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
* 奇盒潮玩 - 全局样式系统
|
||||
*
|
||||
* 基于潮玩盲盒风格的设计系统,采用暖橙渐变色调
|
||||
* 传递惊喜、期待、活力的品牌调性
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
/* ============================================
|
||||
🎨 品牌色彩系统 - 潮玩盲盒 Pro (2025 Refined)
|
||||
============================================ */
|
||||
|
||||
/* 颜色变量 */
|
||||
/* 主色调 - 更加活力与现代 */
|
||||
$brand-primary: #FF6B00; // 核心品牌橙 (更纯粹)
|
||||
$brand-secondary: #FF9500; // 次级品牌色 (用于渐变/辅助)
|
||||
$brand-primary-light: #FF9500; // 亮橙
|
||||
$brand-primary-dark: #E65100; // 深橙
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
/* 辅助色 - 丰富视觉层次 */
|
||||
$accent-gold: #FFC107; // 质感金
|
||||
$accent-red: #FF3B30; // 促销红
|
||||
$accent-blue: #007AFF; // 科技蓝
|
||||
$accent-purple: #AF52DE; // 梦幻紫
|
||||
$accent-pink: #FF2D55; // 活力粉
|
||||
$accent-cyan: #5AC8FA; // 清新蓝
|
||||
$color-success: #34C759; // 成功色
|
||||
$color-warning: #FF9F0A; // 警告色
|
||||
$color-error: #FF3B30; // 错误色
|
||||
|
||||
/* 中性色 - 提升阅读体验 */
|
||||
$text-main: #1D1D1F; // 主要文字 (接近纯黑但柔和)
|
||||
$text-sub: #86868B; // 次要文字
|
||||
$text-secondary: $text-sub; // Alias for compatibility
|
||||
$text-tertiary: #C7C7CC; // 辅助/占位
|
||||
$text-placeholder: $text-tertiary; // Alias for compatibility
|
||||
$text-disabled: #D1D1D6; // 禁用状态文字
|
||||
$text-inverse: #FFFFFF; // 反白文字
|
||||
|
||||
/* 背景色 - 营造氛围 */
|
||||
$bg-page: #F5F5F7; // 页面底色 (高级灰)
|
||||
$bg-card: #FFFFFF; // 卡片背景
|
||||
$bg-glass: rgba(255, 255, 255, 0.8); // 毛玻璃背景
|
||||
$bg-secondary: #F8F8F8; // 次级背景
|
||||
$bg-grey: #FAFAFA; // 浅灰背景
|
||||
|
||||
/* 渐变色 - 视觉冲击力 */
|
||||
// 使用 CSS 变量在 style 中定义,此处保留 SCSS 变量供编译使用
|
||||
$gradient-brand: linear-gradient(135deg, #FF9500 0%, #FF5E00 100%);
|
||||
$gradient-gold: linear-gradient(135deg, #FFD60A 0%, #FF9F0A 100%);
|
||||
$gradient-red: linear-gradient(135deg, #FF3B30 0%, #D32F2F 100%);
|
||||
$gradient-purple: linear-gradient(135deg, #BF5AF2 0%, #5E5CE6 100%);
|
||||
|
||||
/* ============================================
|
||||
🌑 暗黑/特殊主题变量 (Dark Mode Support)
|
||||
============================================ */
|
||||
$bg-dark: #1A1A2E; // 深邃蓝黑
|
||||
$bg-dark-card: rgba(30, 30, 50, 0.7); // 暗色玻璃卡片
|
||||
$text-dark-main: #FFFFFF; // 暗色模式主字
|
||||
$text-dark-sub: rgba(255, 255, 255, 0.7); // 暗色模式副字
|
||||
$border-dark: rgba(255, 255, 255, 0.1); // 暗色边框
|
||||
|
||||
|
||||
/* ============================================
|
||||
📐 布局与间距
|
||||
============================================ */
|
||||
$font-xxs: 18rpx;
|
||||
$font-xs: 22rpx;
|
||||
$font-sm: 24rpx;
|
||||
$font-md: 28rpx;
|
||||
$font-lg: 32rpx;
|
||||
$font-xl: 36rpx;
|
||||
$font-xxl: 48rpx;
|
||||
|
||||
$spacing-xs: 8rpx;
|
||||
$spacing-sm: 12rpx;
|
||||
$spacing-md: 16rpx;
|
||||
$spacing-lg: 24rpx;
|
||||
$spacing-xl: 32rpx;
|
||||
$spacing-xxl: 48rpx;
|
||||
|
||||
$radius-sm: 8rpx;
|
||||
$radius-md: 16rpx;
|
||||
$radius-lg: 24rpx;
|
||||
$radius-xl: 32rpx;
|
||||
$radius-round: 999rpx;
|
||||
|
||||
$border-color-light: #F0F0F0;
|
||||
$border-color: $border-color-light;
|
||||
|
||||
|
||||
/* ============================================
|
||||
✨ 阴影与质感
|
||||
============================================ */
|
||||
$shadow-xs: 0 1rpx 4rpx rgba(0, 0, 0, 0.02);
|
||||
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
$shadow-md: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
$shadow-card: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
$shadow-float: 0 16rpx 48rpx rgba(255, 107, 0, 0.15); // 悬浮投影
|
||||
$shadow-inner: inset 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
|
||||
$shadow-lg: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||||
$shadow-warm: 0 8rpx 24rpx rgba(255, 107, 53, 0.15);
|
||||
$shadow-glow: 0 4rpx 16rpx rgba(255, 159, 67, 0.4);
|
||||
|
||||
/* ============================================
|
||||
🔄 动画时长
|
||||
============================================ */
|
||||
|
||||
$transition-fast: 0.15s;
|
||||
$transition-normal: 0.25s;
|
||||
$transition-slow: 0.35s;
|
||||
$ease-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
$ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
/* ============================================
|
||||
📱 兼容uni-app官方变量
|
||||
============================================ */
|
||||
|
||||
/* 行为相关颜色 - 使用新配色 */
|
||||
$uni-color-primary: $brand-primary;
|
||||
$uni-color-success: #10B981;
|
||||
$uni-color-warning: $accent-gold;
|
||||
$uni-color-error: $accent-red;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
$uni-text-color: $text-main;
|
||||
$uni-text-color-inverse: $text-inverse;
|
||||
$uni-text-color-grey: $text-tertiary;
|
||||
$uni-text-color-placeholder: $text-tertiary;
|
||||
$uni-text-color-disable: #D1D5DB;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
$uni-bg-color: $bg-card;
|
||||
$uni-bg-color-grey: $bg-page;
|
||||
$uni-bg-color-hover: #FFF0E6;
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
$uni-border-color: #E5E7EB;
|
||||
|
||||
/* 尺寸变量 */
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16px;
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
$uni-img-size-sm: 20px;
|
||||
$uni-img-size-base: 26px;
|
||||
$uni-img-size-lg: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-sm: 4px;
|
||||
$uni-border-radius-base: 8px;
|
||||
$uni-border-radius-lg: 12px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
$uni-spacing-row-sm: 6px;
|
||||
$uni-spacing-row-base: 12px;
|
||||
$uni-spacing-row-lg: 16px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
$uni-spacing-col-sm: 6px;
|
||||
$uni-spacing-col-base: 10px;
|
||||
$uni-spacing-col-lg: 14px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
$uni-opacity-disabled: 0.4;
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
$uni-color-title: $text-main;
|
||||
$uni-font-size-title: 20px;
|
||||
$uni-color-subtitle: $text-sub;
|
||||
$uni-font-size-subtitle: 16px;
|
||||
$uni-color-paragraph: $text-sub;
|
||||
$uni-font-size-paragraph: 15px;
|
||||
|
||||
/* ============================================
|
||||
🛠 工具 Mixins
|
||||
============================================ */
|
||||
|
||||
@mixin text-ellipsis($lines: 1) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
34
说明文档.md
Normal file
34
说明文档.md
Normal file
@ -0,0 +1,34 @@
|
||||
# 说明文档
|
||||
|
||||
## 一、项目规划
|
||||
|
||||
### 1.1 项目概述
|
||||
本项目为 Uni-App 开发的 bindbox-mini 小程序。当前阶段任务为优化现有 UI 设计,重点调整色彩、按钮样式及视觉层级,保持整体布局不变。
|
||||
|
||||
### 1.2 实施方案
|
||||
1. **全局样式分析**:检查 `uni.scss` 及各页面样式,提取现有色彩和组件样式。
|
||||
2. **视觉优化设计**:
|
||||
* 定义新的色彩系统(主色、辅助色、中性色)。
|
||||
* 优化按钮样式(圆角、阴影、交互态)。
|
||||
* 提升卡片及列表的视觉质感。
|
||||
3. **代码实现**:
|
||||
* 更新 `uni.scss` 变量。
|
||||
* 重构主要组件/页面的样式代码。
|
||||
4. **验证与测试**:确保各页面视觉一致且无布局崩坏。
|
||||
|
||||
### 1.3 进度记录
|
||||
* [x] 2025-12-17: 初始化说明文档,启动 UI 优化任务。
|
||||
* [x] 2025-12-17: 完成全局 SCSS 变量定义 (`uni.scss`),建立色彩与设计系统。
|
||||
* [x] 2025-12-17: 优化 `pages/index/index.vue` (首页),集成新设计系统与微交互动画。
|
||||
* [x] 2025-12-17: 优化 `pages/activity/pata/index.vue` (帕塔页面),增强动画与视觉质感。
|
||||
* [x] 2025-12-17: 优化 `pages/activity/wuxianshang/index.vue` (无限赏),统一视觉风格。
|
||||
* [x] 2025-12-17: 优化 `pages/mine/index.vue` (个人中心),实现磨砂玻璃效果与动态交互。
|
||||
* [x] 2025-12-17: 优化 `pages/orders/index.vue` (订单列表),增强状态标识与列表质感。
|
||||
* [x] 2025-12-17: 修复 `pages/address` 模块编译错误,并完成地址管理页面的样式标准化优化。
|
||||
* [x] 2025-12-17: 修复 `components/ElCard.vue` 和 `components/YifanSelector.vue` 中的未定义变量错误,统一组件样式。
|
||||
* [x] 2025-12-17: 优化 `pages/activity/wuxianshang/index.vue` (无限赏),替换硬编码样式为全局变量。
|
||||
* [x] 2025-12-17: 修复 `pages/activity/yifanshang/index.vue` 编译错误,在 `uni.scss` 中补充 `text-ellipsis` mixin 定义。
|
||||
* [x] 2025-12-17: 修复 `pages/login/index.vue` 等多处 `$border-color` 未定义错误,在 `uni.scss` 中增加变量别名。
|
||||
* [x] 2025-12-17: 修复 `pages/mine/index.vue` 编译错误,在 `api/appUser.js` 中补充 `getUserInfo`, `getUserTasks`, `getInviteRecords` 导出。
|
||||
* [ ] 2025-12-17: 进行中 - 优化 `pages/activity/yifanshang/index.vue` 及相关组件。
|
||||
* [ ] 2025-12-17: 待开始 - 优化 `pages/login/index.vue` 视觉细节。
|
||||
Loading…
x
Reference in New Issue
Block a user