优化UI
This commit is contained in:
parent
bdd329eb15
commit
fd252efae1
@ -152,6 +152,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 createShippingFeeOrder(user_id, ids) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory/shipping-fee/preorder`, method: 'POST', data: { inventory_ids: ids } })
|
||||
}
|
||||
|
||||
export function cancelShipping(user_id, batch_no) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory/cancel-shipping`, method: 'POST', data: { batch_no } })
|
||||
}
|
||||
@ -188,10 +192,6 @@ 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 transferCoupon(user_id, user_coupon_id, receiver_id) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/coupons/${user_coupon_id}/transfer`, method: 'POST', data: { receiver_id } })
|
||||
}
|
||||
|
||||
export function redeemCoupon(user_id, code) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/coupons/redeem`, method: 'POST', data: { code } })
|
||||
}
|
||||
|
||||
@ -427,7 +427,7 @@ function handleClose() {
|
||||
.title {
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
|
||||
background: $gradient-brand;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@ -451,7 +451,7 @@ function handleClose() {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
color: #667EEA;
|
||||
color: $brand-primary;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
@ -500,7 +500,7 @@ function handleClose() {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6rpx;
|
||||
background: linear-gradient(90deg, #667EEA 0%, #764BA2 100%);
|
||||
background: $gradient-brand;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
@ -607,7 +607,7 @@ function handleClose() {
|
||||
}
|
||||
|
||||
.btn-buy {
|
||||
background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
|
||||
background: $gradient-brand;
|
||||
color: #FFF;
|
||||
font-size: 26rpx;
|
||||
padding: 0 28rpx;
|
||||
@ -657,7 +657,7 @@ function handleClose() {
|
||||
line-height: 44rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
color: #667EEA;
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
@ -673,8 +673,8 @@ function handleClose() {
|
||||
color: #9CA3AF;
|
||||
|
||||
&:active {
|
||||
color: #667EEA;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: $brand-primary;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -699,9 +699,9 @@ function handleClose() {
|
||||
|
||||
// 选中套餐状态
|
||||
.package-item.selected {
|
||||
border-color: #667EEA;
|
||||
background: linear-gradient(145deg, #F8F9FF 0%, #EEF0FF 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.2);
|
||||
border-color: $brand-primary;
|
||||
background: linear-gradient(145deg, #FFF8F4 0%, #FFF0E6 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba($brand-primary, 0.2);
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
@ -826,7 +826,7 @@ function handleClose() {
|
||||
}
|
||||
|
||||
.btn-checkout {
|
||||
background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
|
||||
background: $gradient-brand;
|
||||
color: #FFF;
|
||||
font-size: 32rpx;
|
||||
padding: 0 48rpx;
|
||||
|
||||
@ -41,7 +41,9 @@
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
"ios" : {
|
||||
"dSYMs" : false
|
||||
},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
|
||||
@ -2582,8 +2582,8 @@ onLoad((opts) => {
|
||||
color: #fff;
|
||||
|
||||
&.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
|
||||
background: $gradient-brand;
|
||||
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.4);
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
|
||||
@ -110,7 +110,7 @@ export default {
|
||||
const targetCode = code || this.gameCode
|
||||
if (targetCode !== 'minesweeper_free' && this.ticketCount <= 0) return
|
||||
if (this.entering) return
|
||||
|
||||
|
||||
this.entering = true
|
||||
try {
|
||||
const res = await authRequest({
|
||||
@ -120,16 +120,10 @@ export default {
|
||||
game_code: targetCode
|
||||
}
|
||||
})
|
||||
|
||||
// const clientUrl = res.client_url || 'https://kdy.1024tool.vip'
|
||||
const clientUrl = 'http://localhost:8081/'
|
||||
|
||||
const nakamaServer = res.nakama_server || 'wss://kdy.1024tool.vip/ws'
|
||||
const userInfo = uni.getStorageSync('user_info') || {}
|
||||
const nickname = userInfo.nickname || userInfo.name || '玩家'
|
||||
const gameUrl = `${clientUrl}?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}&game_type=${encodeURIComponent(targetCode)}&nickname=${encodeURIComponent(nickname)}`
|
||||
console.log('=== 游戏URL(复制到Chrome打开) ===', gameUrl)
|
||||
uni.navigateTo({
|
||||
url: `/pages-game/game/webview?url=${encodeURIComponent(gameUrl)}`
|
||||
url: `/pages-game/game/minesweeper/play?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}`
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
@ -151,12 +145,9 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
// const clientUrl = res.client_url || 'https://kdy.1024tool.vip'
|
||||
const clientUrl = 'http://localhost:8081/'
|
||||
const nakamaServer = res.nakama_server || 'wss://kdy.1024tool.vip/ws'
|
||||
const gameUrl = `${clientUrl}?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}&mode=spectate`
|
||||
uni.navigateTo({
|
||||
url: `/pages-game/game/webview?url=${encodeURIComponent(gameUrl)}`
|
||||
url: `/pages-game/game/minesweeper/room-list?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}`
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '无法获取对战列表', icon: 'none' })
|
||||
|
||||
@ -94,7 +94,6 @@
|
||||
|
||||
<!-- 优化后的按钮位置 -->
|
||||
<view class="coupon-action-wrapper" v-if="currentTab === 1">
|
||||
<view class="transfer-link" @click.stop="onTransferCoupon(item)">转赠给好友</view>
|
||||
<view class="use-btn" @click.stop="onUseCoupon(item)">
|
||||
<text class="btn-text">去使用</text>
|
||||
<view class="btn-shine"></view>
|
||||
@ -119,8 +118,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getUserCoupons, transferCoupon } from '../../api/appUser'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getUserCoupons } from '../../api/appUser'
|
||||
import { vibrateShort } from '@/utils/vibrate.js'
|
||||
|
||||
const list = ref([])
|
||||
@ -321,43 +320,6 @@ function onUseCoupon(item) {
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 转赠优惠券
|
||||
function onTransferCoupon(item) {
|
||||
vibrateShort()
|
||||
uni.showModal({
|
||||
title: '转赠优惠券',
|
||||
content: '请输入接收方用户 ID',
|
||||
editable: true,
|
||||
placeholderText: '请输入用户ID',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const receiverId = parseInt(res.content)
|
||||
if (isNaN(receiverId)) {
|
||||
uni.showToast({ title: '请输入有效的用户ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '转赠中...' })
|
||||
try {
|
||||
const userId = getUserId()
|
||||
await transferCoupon(userId, item.id, receiverId)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '转赠成功', icon: 'success' })
|
||||
// 刷新列表
|
||||
onRefresh()
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
title: '转赠失败',
|
||||
content: e.message || '操作失败',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
fetchData()
|
||||
})
|
||||
@ -717,17 +679,7 @@ onLoad(() => {
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.transfer-link {
|
||||
font-size: 22rpx;
|
||||
color: $brand-primary;
|
||||
text-decoration: underline;
|
||||
opacity: 0.8;
|
||||
&:active { opacity: 1; }
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.use-btn {
|
||||
|
||||
@ -570,10 +570,10 @@ onLoad(() => {
|
||||
}
|
||||
|
||||
.use-btn {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
background: $gradient-brand;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 150, 250, 0.2);
|
||||
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
@ -1001,9 +1001,9 @@ function exportReceipt() {
|
||||
gap: 12rpx;
|
||||
margin-top: $spacing-lg;
|
||||
padding: 20rpx 32rpx;
|
||||
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
|
||||
background: $gradient-brand;
|
||||
border-radius: 40rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.3);
|
||||
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
|
||||
@ -1,56 +1,125 @@
|
||||
<template>
|
||||
<view class="synthesis-wrap">
|
||||
<view class="page-title">碎片合成</view>
|
||||
<view class="page-container">
|
||||
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||
<view class="bg-decoration"></view>
|
||||
|
||||
<view v-if="loading" class="status-text">加载中...</view>
|
||||
<view v-else-if="recipes.length === 0" class="status-text">暂无可用的合成配方</view>
|
||||
<!-- 装饰光球 -->
|
||||
<view class="orb orb-1"></view>
|
||||
<view class="orb orb-2"></view>
|
||||
|
||||
<view v-for="recipe in recipes" :key="recipe.id" class="recipe-card">
|
||||
<!-- 目标商品 -->
|
||||
<view class="target-section">
|
||||
<image v-if="recipe.target_product" :src="getFirstImage(recipe.target_product.images_json)" mode="aspectFill" class="target-image" />
|
||||
<view class="target-info">
|
||||
<text class="target-name">{{ recipe.target_product?.name || '目标商品' }}</text>
|
||||
<text class="recipe-name">{{ recipe.name }}</text>
|
||||
<text class="recipe-desc" v-if="recipe.description">{{ recipe.description }}</text>
|
||||
</view>
|
||||
<view class="header-area">
|
||||
<view class="page-title">碎片合成</view>
|
||||
<view class="page-subtitle">Fragment Synthesis</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="content-scroll"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<view class="spinner"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 材料列表 -->
|
||||
<view class="materials-section">
|
||||
<text class="section-label">所需材料</text>
|
||||
<view class="materials-grid">
|
||||
<view v-for="(mat, idx) in recipe.materials" :key="idx" class="material-item">
|
||||
<image :src="mat.image || '/static/placeholder.png'" mode="aspectFill" class="material-image" />
|
||||
<text class="material-name">{{ mat.name }}</text>
|
||||
<view class="material-count" :class="{ enough: mat.owned_count >= mat.required_count }">
|
||||
<text>{{ mat.owned_count }}</text>
|
||||
<text class="count-sep">/</text>
|
||||
<text>{{ mat.required_count }}</text>
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="recipes.length === 0" class="empty-state">
|
||||
<text class="empty-icon">🧩</text>
|
||||
<text class="empty-text">暂无可用的合成配方</text>
|
||||
<text class="empty-hint">敬请期待更多合成方案</text>
|
||||
</view>
|
||||
|
||||
<!-- 配方卡片列表 -->
|
||||
<view v-else class="recipe-list">
|
||||
<view
|
||||
v-for="(recipe, index) in recipes"
|
||||
:key="recipe.id"
|
||||
class="recipe-ticket"
|
||||
:class="{ 'ticket-ready': recipe.can_synthesize }"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
>
|
||||
<!-- 左侧:目标商品主视觉 -->
|
||||
<view class="ticket-left">
|
||||
<view class="product-img-wrap">
|
||||
<image
|
||||
v-if="recipe.target_product"
|
||||
:src="getFirstImage(recipe.target_product.images_json)"
|
||||
mode="aspectFill"
|
||||
class="product-img"
|
||||
/>
|
||||
<view v-else class="product-img-placeholder">
|
||||
<text>🎁</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 可合成光晕 -->
|
||||
<view v-if="recipe.can_synthesize" class="ready-glow"></view>
|
||||
<view class="product-label">
|
||||
<text class="label-text" :class="recipe.can_synthesize ? 'label-ready' : 'label-lack'">
|
||||
{{ recipe.can_synthesize ? '可合成' : '待收集' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分割线(带缺口) -->
|
||||
<view class="ticket-divider">
|
||||
<view class="notch notch-top"></view>
|
||||
<view class="divider-line"></view>
|
||||
<view class="notch notch-bottom"></view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧:配方信息 -->
|
||||
<view class="ticket-right">
|
||||
<view class="ticket-info">
|
||||
<text class="product-name">{{ recipe.target_product?.name || '目标商品' }}</text>
|
||||
<text class="recipe-name">{{ recipe.name }}</text>
|
||||
<text class="recipe-desc" v-if="recipe.description">{{ recipe.description }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 材料清单 -->
|
||||
<view class="materials-row">
|
||||
<view
|
||||
v-for="(mat, idx) in recipe.materials"
|
||||
:key="idx"
|
||||
class="mat-chip"
|
||||
:class="mat.owned_count >= mat.required_count ? 'mat-ok' : 'mat-lack'"
|
||||
>
|
||||
<text class="mat-name">{{ mat.name }}</text>
|
||||
<text class="mat-num">{{ mat.owned_count }}/{{ mat.required_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部:进度 + 按钮 -->
|
||||
<view class="ticket-footer">
|
||||
<text class="ready-hint">
|
||||
{{ getReadyCount(recipe) }}/{{ recipe.materials?.length || 0 }} 材料就绪
|
||||
</text>
|
||||
<view
|
||||
class="synth-btn"
|
||||
:class="recipe.can_synthesize ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ synthesizing ? '合成中' : (recipe.can_synthesize ? '合成' : '不足') }}</text>
|
||||
<view v-if="recipe.can_synthesize" class="btn-shine"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 合成按钮 -->
|
||||
<button
|
||||
class="synthesis-btn"
|
||||
:class="{ disabled: !recipe.can_synthesize }"
|
||||
:disabled="!recipe.can_synthesize || synthesizing"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
{{ recipe.can_synthesize ? '立即合成' : '材料不足' }}
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getSynthesisRecipes, doSynthesis } from '../../api/synthesis.js'
|
||||
|
||||
const loading = ref(true)
|
||||
const synthesizing = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const recipes = ref([])
|
||||
|
||||
function getFirstImage(imagesJson) {
|
||||
@ -63,6 +132,16 @@ function getFirstImage(imagesJson) {
|
||||
}
|
||||
}
|
||||
|
||||
function getReadyCount(recipe) {
|
||||
if (!recipe.materials) return 0
|
||||
return recipe.materials.filter(m => m.owned_count >= m.required_count).length
|
||||
}
|
||||
|
||||
function getOverallProgress(recipe) {
|
||||
if (!recipe.materials || recipe.materials.length === 0) return 0
|
||||
return Math.round((getReadyCount(recipe) / recipe.materials.length) * 100)
|
||||
}
|
||||
|
||||
async function loadRecipes() {
|
||||
loading.value = true
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
@ -80,6 +159,12 @@ async function loadRecipes() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
isRefreshing.value = true
|
||||
await loadRecipes()
|
||||
isRefreshing.value = false
|
||||
}
|
||||
|
||||
async function onSynthesize(recipe) {
|
||||
if (synthesizing.value || !recipe.can_synthesize) return
|
||||
try {
|
||||
@ -108,11 +193,10 @@ async function onSynthesize(recipe) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onLoad(() => {
|
||||
loadRecipes()
|
||||
})
|
||||
|
||||
// 支持从其他页面返回时刷新
|
||||
const onShow = () => {
|
||||
if (!loading.value) loadRecipes()
|
||||
}
|
||||
@ -120,155 +204,413 @@ defineExpose({ onShow })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.synthesis-wrap {
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
background: $bg-page;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 装饰光球 */
|
||||
.orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80rpx);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.orb-1 {
|
||||
width: 500rpx;
|
||||
height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.25), transparent 70%);
|
||||
top: -80rpx;
|
||||
right: -80rpx;
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
.orb-2 {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.15), transparent 70%);
|
||||
bottom: 200rpx;
|
||||
left: -100rpx;
|
||||
animation: float 14s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(20rpx, 30rpx); }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header-area {
|
||||
padding: $spacing-xl $spacing-lg;
|
||||
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 20rpx 0 30rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 1rpx;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 100rpx 0;
|
||||
font-size: 28rpx;
|
||||
.page-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: $text-tertiary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.recipe-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
/* 滚动区 */
|
||||
.content-scroll {
|
||||
height: calc(100vh - 220rpx);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.target-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.target-image {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.target-info {
|
||||
flex: 1;
|
||||
/* 加载 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.recipe-name {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.recipe-desc {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.materials-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.materials-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx $spacing-lg;
|
||||
color: $text-tertiary;
|
||||
font-size: 26rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.material-item {
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 160rpx;
|
||||
padding: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16rpx;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.material-image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.material-name {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
.empty-text {
|
||||
color: $text-tertiary;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.material-count {
|
||||
.empty-hint {
|
||||
color: $text-tertiary;
|
||||
font-size: 24rpx;
|
||||
color: #ff5252;
|
||||
font-weight: bold;
|
||||
margin-top: 4rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.enough {
|
||||
color: #4caf50;
|
||||
}
|
||||
/* 配方列表 */
|
||||
.recipe-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
padding: 0 $spacing-lg $spacing-xl;
|
||||
}
|
||||
|
||||
.count-sep {
|
||||
margin: 0 2rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
/* 票券式卡片 */
|
||||
.recipe-ticket {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-card;
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
position: relative;
|
||||
|
||||
&.ticket-ready {
|
||||
box-shadow:
|
||||
$shadow-card,
|
||||
0 0 0 1.5rpx rgba($brand-primary, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.synthesis-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(24rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 左侧商品区 */
|
||||
.ticket-left {
|
||||
width: 200rpx;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(145deg, #FFF5EC, #FFF0E0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28rpx 16rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-ready .ticket-left {
|
||||
background: linear-gradient(145deg, #FFF5EC, #FFE8C8);
|
||||
}
|
||||
|
||||
/* 可合成光晕 */
|
||||
.ready-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 50% 40%, rgba($brand-primary, 0.12), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.product-img-wrap {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
.product-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-img-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.product-label {
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
|
||||
&.label-ready {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
opacity: 0.85;
|
||||
&.label-lack {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分割线(票券缺口效果) */
|
||||
.ticket-divider {
|
||||
width: 28rpx;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notch {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background: $bg-page;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
&.notch-top { top: -14rpx; }
|
||||
&.notch-bottom { bottom: -14rpx; }
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
width: 0;
|
||||
height: 75%;
|
||||
border-left: 2rpx dashed rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 右侧信息区 */
|
||||
.ticket-right {
|
||||
flex: 1;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
letter-spacing: 0.5rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recipe-name {
|
||||
font-size: 22rpx;
|
||||
color: $text-sub;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recipe-desc {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 材料芯片行 */
|
||||
.materials-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.mat-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 5rpx 12rpx;
|
||||
border-radius: 100rpx;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border: 1.5rpx solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
&.mat-ok {
|
||||
background: rgba($color-success, 0.08);
|
||||
border-color: rgba($color-success, 0.2);
|
||||
}
|
||||
|
||||
&.mat-lack {
|
||||
background: rgba($color-error, 0.06);
|
||||
border-color: rgba($color-error, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-name {
|
||||
font-size: 20rpx;
|
||||
color: $text-sub;
|
||||
max-width: 100rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mat-num {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: $text-tertiary;
|
||||
|
||||
.mat-ok & { color: $color-success; }
|
||||
.mat-lack & { color: $color-error; }
|
||||
}
|
||||
|
||||
/* 底部行:进度提示 + 按钮 */
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.ready-hint {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 合成按钮 - 小胶囊 */
|
||||
.synth-btn {
|
||||
height: 56rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||
|
||||
&.btn-ready {
|
||||
background: $gradient-brand;
|
||||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-locked {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.btn-ready & { color: #fff; }
|
||||
.btn-locked & { color: $text-tertiary; }
|
||||
}
|
||||
|
||||
.btn-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 60%;
|
||||
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%, 100% { left: 200%; }
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.spinner {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
border: 3rpx solid $bg-secondary;
|
||||
border-top-color: $brand-primary;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -69,6 +69,13 @@
|
||||
<view class="task-meta">
|
||||
<text class="task-name">{{ task.name }}</text>
|
||||
<text class="task-desc">{{ task.description }}</text>
|
||||
<view class="task-time-row" v-if="getTaskTimeRangeText(task) || getTaskCountdownText(task, nowMs)">
|
||||
<text class="task-time-range" v-if="getTaskTimeRangeText(task)">{{ getTaskTimeRangeText(task) }}</text>
|
||||
<text class="task-time-countdown" :class="{ expired: getTaskCountdownText(task, nowMs) === '已截止' }" v-if="getTaskCountdownText(task, nowMs)">
|
||||
{{ getTaskCountdownText(task, nowMs) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="task-rule-tip" v-if="getTaskRuleTip(task)">{{ getTaskRuleTip(task) }}</text>
|
||||
|
||||
<!-- 新增:独立进度展示 (当存在 sub_progress 时显示) -->
|
||||
<view class="sub-progress-list" v-if="taskProgress[task.id]?.subProgress?.length > 0">
|
||||
@ -161,15 +168,18 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import { getTasks, getTaskProgress, claimTaskReward } from '../../api/appUser'
|
||||
import { vibrateShort } from '@/utils/vibrate.js'
|
||||
import { parseTimeMs } from '@/utils/format.js'
|
||||
|
||||
const tasks = ref([])
|
||||
const loading = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const expandedTasks = reactive({})
|
||||
const claiming = reactive({})
|
||||
const nowMs = ref(Date.now())
|
||||
let countdownTimer = null
|
||||
|
||||
// 用户进度 (汇总 - 用于顶部统计卡片显示)
|
||||
const userProgress = reactive({
|
||||
@ -219,14 +229,133 @@ function getTaskIcon(task) {
|
||||
return '⭐'
|
||||
}
|
||||
|
||||
function parseTaskTimeMs(value) {
|
||||
const ms = parseTimeMs(value)
|
||||
if (!ms || ms <= 0) return null
|
||||
return ms
|
||||
}
|
||||
|
||||
function formatMonthDay(timestampMs) {
|
||||
if (!timestampMs) return ''
|
||||
const date = new Date(timestampMs)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return `${date.getMonth() + 1}.${date.getDate()}`
|
||||
}
|
||||
|
||||
function getTaskTimeRangeText(task) {
|
||||
const startMs = parseTaskTimeMs(task?.start_time)
|
||||
const endMs = parseTaskTimeMs(task?.end_time)
|
||||
if (startMs && endMs) return `活动时间 ${formatMonthDay(startMs)}-${formatMonthDay(endMs)}`
|
||||
if (startMs) return `活动开始 ${formatMonthDay(startMs)}`
|
||||
if (endMs) return `活动截止 ${formatMonthDay(endMs)}`
|
||||
return ''
|
||||
}
|
||||
|
||||
function formatCountdown(diffMs) {
|
||||
const totalMinutes = Math.ceil(diffMs / (60 * 1000))
|
||||
if (totalMinutes <= 0) return '0分钟'
|
||||
const day = Math.floor(totalMinutes / (24 * 60))
|
||||
const hour = Math.floor((totalMinutes % (24 * 60)) / 60)
|
||||
const minute = totalMinutes % 60
|
||||
if (day > 0) {
|
||||
if (hour > 0) return `${day}天${hour}小时`
|
||||
return `${day}天`
|
||||
}
|
||||
if (hour > 0) {
|
||||
if (minute > 0) return `${hour}小时${minute}分钟`
|
||||
return `${hour}小时`
|
||||
}
|
||||
return `${minute}分钟`
|
||||
}
|
||||
|
||||
function getTaskCountdownText(task, currentMs = Date.now()) {
|
||||
const endMs = parseTaskTimeMs(task?.end_time)
|
||||
if (!endMs) return ''
|
||||
const diffMs = endMs - currentMs
|
||||
if (diffMs <= 0) return '已截止'
|
||||
return `距截止 ${formatCountdown(diffMs)}`
|
||||
}
|
||||
|
||||
function hasDailyWindow(task) {
|
||||
const tiers = task?.tiers
|
||||
if (!Array.isArray(tiers) || tiers.length === 0) return false
|
||||
return tiers.some(tier => String(tier?.window || '').toLowerCase() === 'daily')
|
||||
}
|
||||
|
||||
function getTaskRuleTip(task) {
|
||||
if (hasDailyWindow(task)) return '按当日消费统计,次日重置'
|
||||
return ''
|
||||
}
|
||||
|
||||
function startCountdownTimer() {
|
||||
stopCountdownTimer()
|
||||
countdownTimer = setInterval(() => {
|
||||
nowMs.value = Date.now()
|
||||
}, 60 * 1000)
|
||||
}
|
||||
|
||||
function stopCountdownTimer() {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function buildWindowedSubProgress(task, progressData) {
|
||||
const tierList = Array.isArray(task?.tiers) ? task.tiers : []
|
||||
const tierProgressList = Array.isArray(progressData?.tier_progress) ? progressData.tier_progress : []
|
||||
if (tierList.length === 0 || tierProgressList.length === 0) return []
|
||||
|
||||
const metricWhitelist = new Set(['order_amount', 'order_count'])
|
||||
const relevantTiers = tierList.filter(tier => metricWhitelist.has(tier?.metric))
|
||||
if (relevantTiers.length === 0) return []
|
||||
|
||||
const tierProgressMap = new Map(
|
||||
tierProgressList.map(tp => [Number(tp?.tier_id || 0), tp])
|
||||
)
|
||||
|
||||
const activityOrder = []
|
||||
const activitySeen = new Set()
|
||||
const activityStatsMap = new Map()
|
||||
|
||||
relevantTiers.forEach(tier => {
|
||||
const activityId = Number(tier?.activity_id || 0)
|
||||
if (!activitySeen.has(activityId)) {
|
||||
activitySeen.add(activityId)
|
||||
activityOrder.push(activityId)
|
||||
}
|
||||
|
||||
const current = activityStatsMap.get(activityId) || {
|
||||
activity_id: activityId,
|
||||
order_amount: 0,
|
||||
order_count: 0
|
||||
}
|
||||
|
||||
const tp = tierProgressMap.get(Number(tier?.id || 0))
|
||||
if (tp) {
|
||||
if (tier.metric === 'order_amount') {
|
||||
current.order_amount = Math.max(current.order_amount, Number(tp.order_amount || 0))
|
||||
} else if (tier.metric === 'order_count') {
|
||||
current.order_count = Math.max(current.order_count, Number(tp.order_count || 0))
|
||||
}
|
||||
}
|
||||
|
||||
activityStatsMap.set(activityId, current)
|
||||
})
|
||||
|
||||
return activityOrder.map(activityId => activityStatsMap.get(activityId)).filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeSubProgress(task, progressData) {
|
||||
const rawList = Array.isArray(progressData?.sub_progress)
|
||||
const windowedList = buildWindowedSubProgress(task, progressData)
|
||||
const fallbackList = Array.isArray(progressData?.sub_progress)
|
||||
? progressData.sub_progress.map(item => ({
|
||||
activity_id: item.activity_id,
|
||||
order_amount: item.order_amount || 0,
|
||||
order_count: item.order_count || 0
|
||||
}))
|
||||
: []
|
||||
const rawList = windowedList.length > 0 ? [...windowedList] : fallbackList
|
||||
const hasGlobalTier = (task?.tiers || []).some(
|
||||
t => (t.activity_id || 0) === 0 && (t.metric === 'order_amount' || t.metric === 'order_count')
|
||||
)
|
||||
@ -383,16 +512,32 @@ function isTierClaimed(taskId, tierId) {
|
||||
return claimed.includes(tierId)
|
||||
}
|
||||
|
||||
// 是否可领取 - BUG修复:使用任务独立的进度数据
|
||||
// 是否可领取 - 优先使用 tier 级别窗口化进度(与后端 ClaimTier 一致)
|
||||
function isTierClaimable(task, tier) {
|
||||
const metric = tier.metric || ''
|
||||
const threshold = tier.threshold || 0
|
||||
const operator = tier.operator || '>='
|
||||
|
||||
// 获取该任务独立的进度数据
|
||||
|
||||
const progress = taskProgress[task.id] || {}
|
||||
|
||||
// FIX: 如果档位关联了特定活动,优先使用活动独立进度 (sub_progress)
|
||||
|
||||
// 优先使用 tier 级别窗口化进度(与后端 ClaimTier 使用同一数据源)
|
||||
if (progress.tierProgress && progress.tierProgress.length > 0) {
|
||||
const tp = progress.tierProgress.find(t => t.tier_id === tier.id)
|
||||
if (tp) {
|
||||
let current = 0
|
||||
if (metric === 'first_order') return tp.first_order || false
|
||||
else if (metric === 'order_count') current = tp.order_count || 0
|
||||
else if (metric === 'order_amount') current = tp.order_amount || 0
|
||||
else if (metric === 'invite_count') current = tp.invite_count || 0
|
||||
|
||||
if (operator === '>=') return current >= threshold
|
||||
if (operator === '==') return current === threshold
|
||||
if (operator === '>') return current > threshold
|
||||
return current >= threshold
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:使用 subProgress 或全局进度
|
||||
if (tier.activity_id > 0) {
|
||||
if (progress.subProgress) {
|
||||
const sub = progress.subProgress.find(s => s.activity_id === tier.activity_id)
|
||||
@ -410,15 +555,12 @@ function isTierClaimable(task, tier) {
|
||||
if (operator === '>') return current > threshold
|
||||
return current >= threshold
|
||||
}
|
||||
// 其他指标暂时没有拆分,回退到总进度校验
|
||||
} else {
|
||||
// 没找到该活动的进度记录 -> 视为 0 ->不可领取
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:使用任务总进度
|
||||
let current = 0
|
||||
if (metric === 'first_order') {
|
||||
return progress.firstOrder || false
|
||||
@ -429,40 +571,44 @@ function isTierClaimable(task, tier) {
|
||||
} else if (metric === 'invite_count') {
|
||||
current = progress.inviteCount || 0
|
||||
}
|
||||
|
||||
|
||||
if (operator === '>=') return current >= threshold
|
||||
if (operator === '==') return current === threshold
|
||||
if (operator === '>') return current > threshold
|
||||
return current >= threshold
|
||||
}
|
||||
|
||||
// 获取进度文字 - BUG修复:使用任务独立的进度数据
|
||||
// 获取进度文字 - 优先使用 tier 级别窗口化进度
|
||||
function getTierProgressText(task, tier) {
|
||||
const metric = tier.metric || ''
|
||||
const threshold = tier.threshold || 0
|
||||
|
||||
// 获取该任务独立的进度数据
|
||||
|
||||
const progress = taskProgress[task.id] || {}
|
||||
|
||||
// FIX: 如果档位关联了特定活动,优先显示活动独立进度
|
||||
// 优先使用 tier 级别窗口化进度
|
||||
if (progress.tierProgress && progress.tierProgress.length > 0) {
|
||||
const tp = progress.tierProgress.find(t => t.tier_id === tier.id)
|
||||
if (tp) {
|
||||
if (metric === 'first_order') return tp.first_order ? '已完成' : '未完成'
|
||||
if (metric === 'order_amount') return `¥${(tp.order_amount || 0) / 100}/¥${threshold / 100}`
|
||||
if (metric === 'order_count') return `${tp.order_count || 0}/${threshold}`
|
||||
if (metric === 'invite_count') return `${tp.invite_count || 0}/${threshold}`
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:活动独立进度
|
||||
if (tier.activity_id > 0 && progress.subProgress) {
|
||||
const sub = progress.subProgress.find(s => s.activity_id === tier.activity_id)
|
||||
if (sub) {
|
||||
if (metric === 'order_amount') {
|
||||
const current = sub.order_amount || 0
|
||||
return `¥${current / 100}/¥${threshold / 100}`
|
||||
} else if (metric === 'order_count') {
|
||||
const current = sub.order_count || 0
|
||||
return `${current}/${threshold}`
|
||||
}
|
||||
if (metric === 'order_amount') return `¥${(sub.order_amount || 0) / 100}/¥${threshold / 100}`
|
||||
if (metric === 'order_count') return `${sub.order_count || 0}/${threshold}`
|
||||
} else {
|
||||
// 没找到记录 -> 0
|
||||
if (metric === 'order_amount') return `¥0/¥${threshold / 100}`
|
||||
return `0/${threshold}`
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:使用任务总进度
|
||||
|
||||
// 回退:任务总进度
|
||||
let current = 0
|
||||
if (metric === 'first_order') {
|
||||
return progress.firstOrder ? '已完成' : '未完成'
|
||||
@ -474,7 +620,7 @@ function getTierProgressText(task, tier) {
|
||||
} else if (metric === 'invite_count') {
|
||||
current = progress.inviteCount || 0
|
||||
}
|
||||
|
||||
|
||||
return `${current}/${threshold}`
|
||||
}
|
||||
|
||||
@ -561,7 +707,8 @@ async function fetchData() {
|
||||
orderAmount: p.order_amount || 0,
|
||||
inviteCount: p.invite_count || 0,
|
||||
firstOrder: p.first_order || false,
|
||||
subProgress: normalizedSubProgress // 新增:独立进度列表
|
||||
subProgress: normalizedSubProgress,
|
||||
tierProgress: p.tier_progress || []
|
||||
}
|
||||
|
||||
// 聚合进度指标 (取各任务返回的最大值 - 仅用于顶部统计卡片显示)
|
||||
@ -584,13 +731,18 @@ async function fetchData() {
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
nowMs.value = Date.now()
|
||||
startCountdownTimer()
|
||||
fetchData()
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
stopCountdownTimer()
|
||||
})
|
||||
|
||||
// 计算普通档位进度条百分比
|
||||
function getTierProgressPercent(task, tier) {
|
||||
const metric = tier.metric || ''
|
||||
// first_order 是布尔值,不显示进度条
|
||||
if (metric === 'first_order') return ''
|
||||
|
||||
const threshold = tier.threshold || 0
|
||||
@ -600,27 +752,28 @@ function getTierProgressPercent(task, tier) {
|
||||
|
||||
let current = 0
|
||||
|
||||
// 如果档位关联了特定活动,从 subProgress 取值
|
||||
// 优先使用 tier 级别窗口化进度
|
||||
if (progress.tierProgress && progress.tierProgress.length > 0) {
|
||||
const tp = progress.tierProgress.find(t => t.tier_id === tier.id)
|
||||
if (tp) {
|
||||
if (metric === 'order_count') current = tp.order_count || 0
|
||||
else if (metric === 'order_amount') current = tp.order_amount || 0
|
||||
else if (metric === 'invite_count') current = tp.invite_count || 0
|
||||
return Math.min(current / threshold * 100, 100).toFixed(0) + '%'
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:活动独立进度
|
||||
if (tier.activity_id > 0 && progress.subProgress) {
|
||||
const sub = progress.subProgress.find(s => s.activity_id === tier.activity_id)
|
||||
if (sub) {
|
||||
if (metric === 'order_amount') {
|
||||
current = sub.order_amount || 0
|
||||
} else if (metric === 'order_count') {
|
||||
current = sub.order_count || 0
|
||||
}
|
||||
} else {
|
||||
current = 0
|
||||
if (metric === 'order_amount') current = sub.order_amount || 0
|
||||
else if (metric === 'order_count') current = sub.order_count || 0
|
||||
}
|
||||
} else {
|
||||
// 使用任务总进度
|
||||
if (metric === 'order_count') {
|
||||
current = progress.orderCount || 0
|
||||
} else if (metric === 'order_amount') {
|
||||
current = progress.orderAmount || 0
|
||||
} else if (metric === 'invite_count') {
|
||||
current = progress.inviteCount || 0
|
||||
}
|
||||
if (metric === 'order_count') current = progress.orderCount || 0
|
||||
else if (metric === 'order_amount') current = progress.orderAmount || 0
|
||||
else if (metric === 'invite_count') current = progress.inviteCount || 0
|
||||
}
|
||||
|
||||
return Math.min(current / threshold * 100, 100).toFixed(0) + '%'
|
||||
@ -893,6 +1046,41 @@ function formatAmount(cents) {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.task-time-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.task-time-range {
|
||||
font-size: 22rpx;
|
||||
color: $text-tertiary;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.task-time-countdown {
|
||||
font-size: 22rpx;
|
||||
color: $brand-primary;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
|
||||
&.expired {
|
||||
color: $text-tertiary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.task-rule-tip {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
/* 独立进度条样式 */
|
||||
.sub-progress-list {
|
||||
display: flex;
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<text class="tab-text">已申请发货</text>
|
||||
<text class="tab-count" v-if="shippedList.length > 0">({{ shippedList.length }})</text>
|
||||
</view>
|
||||
<view class="tab-item" @tap="goSynthesis">
|
||||
<view class="tab-item" :class="{ active: currentTab === 2 }" @tap="switchTab(2)">
|
||||
<text class="tab-text">碎片合成</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -52,7 +52,7 @@
|
||||
<text class="item-price" v-if="item.price">单价: ¥{{ item.price }}</text>
|
||||
<view class="item-actions">
|
||||
<text class="invite-btn" v-if="!item.selected && !item.is_fragment" @tap.stop="onInvite(item)">邀请填写</text>
|
||||
<text class="synthesis-link" v-if="!item.selected && item.is_fragment" @tap.stop="goSynthesis">去合成</text>
|
||||
<text class="synthesis-link" v-if="!item.selected && item.is_fragment" @tap.stop="switchTab(2)">去合成</text>
|
||||
<text class="item-count" v-if="!item.selected">x{{ item.count || 1 }}</text>
|
||||
<view class="stepper" v-else @tap.stop>
|
||||
<text class="step-btn minus" @tap.stop="changeCount(item, -1)">-</text>
|
||||
@ -149,6 +149,81 @@
|
||||
<view v-if="loading && shippedList.length > 0" class="loading-more">加载更多...</view>
|
||||
<view v-if="!hasMore && shippedList.length > 0" class="no-more">没有更多了</view>
|
||||
</block>
|
||||
|
||||
<!-- Tab 2: 碎片合成 -->
|
||||
<block v-if="currentTab === 2">
|
||||
<view v-if="synthLoading" class="status-text">加载中...</view>
|
||||
<view v-else-if="recipes.length === 0" class="status-text">暂无可用的合成配方</view>
|
||||
<view v-else class="recipe-list">
|
||||
<view
|
||||
v-for="(recipe, index) in recipes"
|
||||
:key="recipe.id"
|
||||
class="recipe-ticket"
|
||||
:class="{ 'ticket-ready': recipe.can_synthesize }"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
>
|
||||
<!-- 左侧:目标商品主视觉 -->
|
||||
<view class="ticket-left">
|
||||
<view class="product-img-wrap">
|
||||
<image
|
||||
v-if="recipe.target_product"
|
||||
:src="getSynthFirstImage(recipe.target_product.images_json)"
|
||||
mode="aspectFill"
|
||||
class="product-img"
|
||||
/>
|
||||
<view v-else class="product-img-placeholder"><text>🎁</text></view>
|
||||
</view>
|
||||
<view v-if="recipe.can_synthesize" class="ready-glow"></view>
|
||||
<view class="product-label">
|
||||
<text class="label-text" :class="recipe.can_synthesize ? 'label-ready' : 'label-lack'">
|
||||
{{ recipe.can_synthesize ? '可合成' : '待收集' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分割线(带缺口) -->
|
||||
<view class="ticket-divider">
|
||||
<view class="notch notch-top"></view>
|
||||
<view class="divider-line"></view>
|
||||
<view class="notch notch-bottom"></view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧:配方信息 -->
|
||||
<view class="ticket-right">
|
||||
<view class="ticket-info">
|
||||
<text class="product-name">{{ recipe.target_product?.name || '目标商品' }}</text>
|
||||
<text class="recipe-name">{{ recipe.name }}</text>
|
||||
<text class="recipe-desc" v-if="recipe.description">{{ recipe.description }}</text>
|
||||
</view>
|
||||
<view class="materials-row">
|
||||
<view
|
||||
v-for="(mat, idx) in recipe.materials"
|
||||
:key="idx"
|
||||
class="mat-chip"
|
||||
:class="mat.owned_count >= mat.required_count ? 'mat-ok' : 'mat-lack'"
|
||||
>
|
||||
<text class="mat-name">{{ mat.name }}</text>
|
||||
<text class="mat-num">{{ mat.owned_count }}/{{ mat.required_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ticket-footer">
|
||||
<text class="ready-hint">
|
||||
{{ getSynthReadyCount(recipe) }}/{{ recipe.materials?.length || 0 }} 材料就绪
|
||||
</text>
|
||||
<view
|
||||
class="synth-btn"
|
||||
:class="recipe.can_synthesize ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ synthesizing ? '合成中' : (recipe.can_synthesize ? '合成' : '不足') }}</text>
|
||||
<view v-if="recipe.can_synthesize" class="btn-shine"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 分享弹窗 -->
|
||||
<view class="share-mask" v-if="showSharePopup" @tap="showSharePopup = false" @touchmove.stop></view>
|
||||
<view class="share-popup glass-card" :class="{ 'show': showSharePopup }">
|
||||
@ -178,9 +253,11 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare } from '@/api/appUser'
|
||||
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare, createShippingFeeOrder } from '@/api/appUser'
|
||||
import { getSynthesisRecipes, doSynthesis } from '@/api/synthesis.js'
|
||||
import { vibrateShort } from '@/utils/vibrate.js'
|
||||
import { checkPhoneBound, checkPhoneBoundSync } from '@/utils/checkPhone.js'
|
||||
import { executePaymentFlow } from '@/utils/payment.js'
|
||||
// #ifdef MP-TOUTIAO
|
||||
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
|
||||
// #endif
|
||||
@ -201,6 +278,11 @@ const pageSize = ref(100)
|
||||
const hasMore = ref(true)
|
||||
const productMetaCache = new Map()
|
||||
|
||||
// Synthesis tab state
|
||||
const recipes = ref([])
|
||||
const synthLoading = ref(false)
|
||||
const synthesizing = ref(false)
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return aggregatedList.value.reduce((sum, item) => sum + (item.count || 1), 0)
|
||||
})
|
||||
@ -271,6 +353,8 @@ onShow(() => {
|
||||
const uid = uni.getStorageSync("user_id")
|
||||
if (currentTab.value === 1) {
|
||||
loadShipments(uid)
|
||||
} else if (currentTab.value === 2) {
|
||||
loadRecipes(uid)
|
||||
} else {
|
||||
loadInventory(uid) // 改为只加载第一页,后续由 onReachBottom 触发
|
||||
}
|
||||
@ -284,6 +368,8 @@ onPullDownRefresh(() => {
|
||||
if (currentTab.value === 1) {
|
||||
shippedList.value = []
|
||||
loadShipments(uid).finally(() => uni.stopPullDownRefresh())
|
||||
} else if (currentTab.value === 2) {
|
||||
loadRecipes(uid).finally(() => uni.stopPullDownRefresh())
|
||||
} else {
|
||||
aggregatedList.value = []
|
||||
loadInventory(uid).finally(() => uni.stopPullDownRefresh())
|
||||
@ -312,6 +398,8 @@ function switchTab(index) {
|
||||
const uid = uni.getStorageSync("user_id")
|
||||
if (currentTab.value === 1) {
|
||||
loadShipments(uid)
|
||||
} else if (currentTab.value === 2) {
|
||||
loadRecipes(uid)
|
||||
} else {
|
||||
loadInventory(uid) // 改为按需加载
|
||||
}
|
||||
@ -581,8 +669,58 @@ function changeCount(item, delta) {
|
||||
}
|
||||
}
|
||||
|
||||
function goSynthesis() {
|
||||
uni.navigateTo({ url: '/pages-user/synthesis/index' })
|
||||
function getSynthFirstImage(imagesJson) {
|
||||
if (!imagesJson) return '/static/placeholder.png'
|
||||
try {
|
||||
const imgs = JSON.parse(imagesJson)
|
||||
return imgs && imgs.length > 0 ? imgs[0] : '/static/placeholder.png'
|
||||
} catch {
|
||||
return imagesJson
|
||||
}
|
||||
}
|
||||
|
||||
function getSynthReadyCount(recipe) {
|
||||
if (!recipe.materials) return 0
|
||||
return recipe.materials.filter(m => m.owned_count >= m.required_count).length
|
||||
}
|
||||
|
||||
async function loadRecipes(uid) {
|
||||
synthLoading.value = true
|
||||
const userId = uid || uni.getStorageSync('user_id')
|
||||
if (!userId) { synthLoading.value = false; return }
|
||||
try {
|
||||
const res = await getSynthesisRecipes(userId)
|
||||
recipes.value = res?.list || []
|
||||
} catch (e) {
|
||||
console.error('loadRecipes error', e)
|
||||
} finally {
|
||||
synthLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSynthesize(recipe) {
|
||||
if (synthesizing.value || !recipe.can_synthesize) return
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: '确认合成',
|
||||
content: `确定要合成「${recipe.target_product?.name || '目标商品'}」吗?合成后碎片将被消耗。`,
|
||||
success: (res) => res.confirm ? resolve() : reject('cancel'),
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
} catch { return }
|
||||
synthesizing.value = true
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
try {
|
||||
await doSynthesis(userId, recipe.id)
|
||||
uni.showToast({ title: '合成成功!', icon: 'success' })
|
||||
await loadRecipes(userId)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '合成失败', icon: 'none' })
|
||||
} finally {
|
||||
synthesizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRedeem() {
|
||||
@ -681,7 +819,55 @@ async function onShip() {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 有默认地址,确认发货
|
||||
// 2. 有默认地址,判断是否需要支付运费
|
||||
const FREIGHT_THRESHOLD = 5
|
||||
const FREIGHT_FEE = 10
|
||||
|
||||
if (allIds.length > FREIGHT_THRESHOLD) {
|
||||
// 超过 5 件,需支付 10 元运费
|
||||
const confirmed = await new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: '需支付运费',
|
||||
content: `共 ${allIds.length} 件商品,超过 ${FREIGHT_THRESHOLD} 件需支付 ¥${FREIGHT_FEE}.00 运费,确认继续?`,
|
||||
confirmText: '去支付',
|
||||
cancelText: '取消',
|
||||
success: (res) => resolve(res.confirm)
|
||||
})
|
||||
})
|
||||
if (!confirmed) return
|
||||
|
||||
uni.showLoading({ title: '创建订单...' })
|
||||
try {
|
||||
await executePaymentFlow({
|
||||
createOrder: () => createShippingFeeOrder(user_id, allIds),
|
||||
openid: uni.getStorageSync('openid')
|
||||
})
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
if (e?.cancelled) return
|
||||
uni.showToast({ title: e?.message || '支付失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.hideLoading()
|
||||
|
||||
// 支付成功后发货
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
try {
|
||||
await requestShipping(user_id, allIds)
|
||||
uni.showToast({ title: '申请成功', icon: 'success' })
|
||||
aggregatedList.value = []
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
loadInventory(user_id)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 不超过 5 件,原有确认发货流程
|
||||
uni.showModal({
|
||||
title: '确认发货',
|
||||
content: `共 ${allIds.length} 件物品,确认申请发货?`,
|
||||
@ -1462,11 +1648,283 @@ function onCopyShareLink() {
|
||||
|
||||
.synthesis-link {
|
||||
font-size: 24rpx;
|
||||
color: #667eea;
|
||||
color: $brand-primary;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fragment-placeholder {
|
||||
width: 40rpx;
|
||||
}
|
||||
|
||||
/* ── 碎片合成 Tab 样式 ── */
|
||||
.recipe-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx $spacing-lg;
|
||||
margin-top: 108rpx;
|
||||
}
|
||||
|
||||
.recipe-ticket {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-card;
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
|
||||
&.ticket-ready {
|
||||
box-shadow: $shadow-card, 0 0 0 1.5rpx rgba($brand-primary, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(24rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.ticket-left {
|
||||
width: 200rpx;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(145deg, #FFF5EC, #FFF0E0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28rpx 16rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-ready .ticket-left {
|
||||
background: linear-gradient(145deg, #FFF5EC, #FFE8C8);
|
||||
}
|
||||
|
||||
.ready-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 50% 40%, rgba($brand-primary, 0.12), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.product-img-wrap {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-img-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.product-label {
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
|
||||
&.label-ready { color: $brand-primary; }
|
||||
&.label-lack { color: $text-tertiary; }
|
||||
}
|
||||
|
||||
.ticket-divider {
|
||||
width: 28rpx;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notch {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background: $bg-page;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
&.notch-top { top: -14rpx; }
|
||||
&.notch-bottom { bottom: -14rpx; }
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
width: 0;
|
||||
height: 75%;
|
||||
border-left: 2rpx dashed rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ticket-right {
|
||||
flex: 1;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recipe-name {
|
||||
font-size: 22rpx;
|
||||
color: $text-sub;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recipe-desc {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.materials-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.mat-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 5rpx 12rpx;
|
||||
border-radius: 100rpx;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border: 1.5rpx solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
&.mat-ok {
|
||||
background: rgba($color-success, 0.08);
|
||||
border-color: rgba($color-success, 0.2);
|
||||
}
|
||||
&.mat-lack {
|
||||
background: rgba($color-error, 0.06);
|
||||
border-color: rgba($color-error, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-name {
|
||||
font-size: 20rpx;
|
||||
color: $text-sub;
|
||||
max-width: 100rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mat-num {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: $text-tertiary;
|
||||
|
||||
.mat-ok & { color: $color-success; }
|
||||
.mat-lack & { color: $color-error; }
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.ready-hint {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.synth-btn {
|
||||
height: 56rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.btn-ready {
|
||||
background: $gradient-brand;
|
||||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-locked {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.btn-ready & { color: #fff; }
|
||||
.btn-locked & { color: $text-tertiary; }
|
||||
}
|
||||
|
||||
.btn-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 60%;
|
||||
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%, 100% { left: 200%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -778,9 +778,9 @@ export default {
|
||||
.card-match .card-title-small { color: $accent-pink; }
|
||||
|
||||
.card-tower {
|
||||
background: linear-gradient(135deg, #E0C3FC 0%, #8EC5FC 100%); /* 梦幻紫蓝 */
|
||||
background: linear-gradient(135deg, #FFE0CC 0%, #FFCBA4 100%); /* 品牌橙暖色 */
|
||||
}
|
||||
.card-tower .card-title-small { color: $accent-purple; }
|
||||
.card-tower .card-title-small { color: $brand-primary; }
|
||||
|
||||
.card-more {
|
||||
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
|
||||
|
||||
@ -587,13 +587,19 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
background-color: #f7f8fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shop-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -606,12 +612,19 @@ onUnmounted(() => {
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 160rpx;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-right: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
z-index: 10;
|
||||
}
|
||||
.sidebar-scroll { height: 100%; }
|
||||
.sidebar-list { padding: 40rpx 0; }
|
||||
.sidebar-scroll {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.sidebar-list {
|
||||
padding: 40rpx 0 calc(140rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.sidebar-item {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
@ -646,6 +659,7 @@ onUnmounted(() => {
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user