topfans/frontend/pages/castlove/craft-select.vue

445 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<image class="bg-wrapper" src="/static/castlove/beijingban.png" mode="aspectFill"></image>
<view class="main-container">
<view class="cards-container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
<view v-if="currentCardList.length">
<view v-for="(card, index) in currentCardList" :key="index" class="card-item"
:class="{ 'no-transition': disableTransition }" :style="getCardStyle(index)">
<view class="card-frame"
:class="{ 'no-border': card.comingSoon, 'card-frame--tappable': !card.comingSoon }"
@tap.stop="onCardFrameTap(index)">
<image class="card-image" :src="card.image" mode="aspectFill" />
<image v-if="card.comingSoon" class="coming-soon-badge"
src="/static/castlove/jinqingqidai.png" />
</view>
</view>
</view>
<!-- 空态:当前分类暂无卡片时显示 -->
<!-- <view v-else class="empty-state">
<image class="empty-state-icon" src="/static/castlove/jinqingqidai.png" mode="aspectFit" />
<text class="empty-state-title">{{ currentCategoryName }}敬请期待</text>
<text class="empty-state-desc">该分类正在打磨中,马上与您见面</text>
</view> -->
</view>
<view v-if="showMenu" class="text-panel">
<view class="arrow-btn arrow-up" @click="scrollUp">
<image class="arrow-icon" src="/static/castlove/jiantou.png" style="transform:rotate(180deg)" />
</view>
<view class="text-list">
<view v-for="(category, index) in categoryList" :key="index" class="text-item"
:class="{ active: selectedCategoryIndex === index, 'font-large': index === 1, 'font-mid': index === 0 || index === 2 }"
@click="selectCategory(index)">
<text>{{ category.name }}</text>
</view>
</view>
<view class="arrow-btn arrow-down" @click="scrollDown">
<image class="arrow-icon" src="/static/castlove/jiantou.png" />
</view>
</view>
</view>
</view>
</template>
<script>
export default {
// 作为组件被 mall.vue 引入时的入参
// (作为页面被 navigateTo 直接打开时不传值,走 onLoad 读取 URL 参数)
props: {
// 初始分类:star_card | badge | poster
type: {
type: String,
default: '',
},
},
watch: {
// 作为组件时,父组件传 type 进来时自动定位
// 用 immediate + handler 显式写法,避免简写在某些 H5 场景下不触发
type: {
immediate: true,
handler(val) {
this.applyType(val)
},
},
},
created() {
// 组件创建时立即应用一次 type,避免 mall.vue 在 onLoad 才设置 initialType
// 导致首次渲染拿到默认 "" 而 watch 错过的情况
this.applyType(this.type)
},
onLoad(options) {
// 作为页面被直接打开时,隐藏右侧分类菜单栏(根据用户需求)
this.showMenu = false
// 接收从主Tab(square.vue / CastloveContent.vue)传入的 type,自动定位到对应分类
// type 取值:star_card | badge | poster
if (options && options.type) {
this.applyType(options.type)
}
},
onShow() {
try {
uni.hideToast()
uni.hideLoading()
} catch (e) { }
},
data() {
return {
// 是否显示右侧分类菜单栏
// - 作为组件(mall.vue 中)使用时,默认 true
// - 作为页面被直接打开时,onLoad 会把它置为 false
showMenu: true,
selectedCategoryIndex: 0,
// 主Tab 的 type 值与 categoryList 索引的映射
// 与 square.vue / CastloveContent.vue 中 mainTabs 的 type 保持一致
categoryTypeMap: {
star_card: 0,
badge: 1,
poster: 2,
},
categoryList: [{ name: '星卡' }, { name: '吧唧' }, { name: '海报' }],
cardListMap: {
'星卡': [
{ name: '光栅卡', image: '/static/castlove/guangshanka.png', comingSoon: false },
{ name: '拍立得', image: '/static/castlove/pailide.png', comingSoon: false },
{ name: '开发中', image: '/static/castlove/daikaifa.png', comingSoon: true },
{ name: '镭射卡', image: '/static/castlove/leisheka.png', comingSoon: false },
{ name: '撕拉片', image: '/static/castlove/silapian.png', comingSoon: false },
],
'吧唧': [],
'海报': []
},
cardList: [
{ name: '光栅卡', image: '/static/castlove/guangshanka.png', comingSoon: false },
{ name: '拍立得', image: '/static/castlove/pailide.png', comingSoon: false },
{ name: '镭射卡', image: '/static/castlove/leisheka.png', comingSoon: false },
{ name: '撕拉片', image: '/static/castlove/silapian.png', comingSoon: false },
{ name: '开发中', image: '/static/castlove/daikaifa.png', comingSoon: true }
],
cardRoutes: {
'光栅卡': '/pages/castlove/lenticular/lenticular-create',
'拍立得': '/pages/castlove/create',
'镭射卡': '/pages/castlove/create',
'撕拉片': '/pages/castlove/create',
},
totalCard: 5,
selectedIndex: 1, // 默认第2张
touchStartY: 0,
dragOffset: 0,
isDragging: false,
disableTransition: false,
SWIPE_STEP: 100,
}
},
computed: {
currentCategoryName() {
return this.categoryList[this.selectedCategoryIndex]?.name || ''
},
currentCardList() {
const name = this.categoryList[this.selectedCategoryIndex]?.name
return this.cardListMap[name] || this.cardList
}
},
methods: {
// 根据 type 字符串定位到对应分类索引
applyType(type) {
if (!type) return
const idx = this.categoryTypeMap[type]
if (typeof idx === 'number') {
this.selectedCategoryIndex = idx
}
},
selectCategory(index) {
this.selectedCategoryIndex = index
this.selectedIndex = 1
this.dragOffset = 0
this.isDragging = false
},
onTouchStart(e) {
this.touchStartY = e.touches[0].clientY
this.isDragging = true
this.disableTransition = true
},
onTouchMove(e) {
if (!this.isDragging) return
const moveY = e.touches[0].clientY
this.dragOffset = moveY - this.touchStartY
},
onTouchEnd() {
if (!this.isDragging) return
this.isDragging = false
this.disableTransition = false
const moveCount = Math.round(-this.dragOffset / this.SWIPE_STEP)
let newIdx = this.selectedIndex + moveCount
newIdx = ((newIdx % 5) + 5) % 5
this.selectedIndex = newIdx
this.dragOffset = 0
},
// ====================== 核心修复z-index + 层级 + 循环 ======================
getCardStyle(index) {
const positions = [
{ left: 288, top: 64, rotate: 25, scale: 0.75 },
{ left: 120, top: 288, rotate: 12, scale: 0.95 },
{ left: 60, top: 580, rotate: 0, scale: 1 },
{ left: 120, top: 888, rotate: -12, scale: 0.95 },
{ left: 224, top: 1096, rotate: -25, scale: 0.75 },
]
const progress = -this.dragOffset / this.SWIPE_STEP
const centerIdx = this.selectedIndex + progress
// 循环最短差值(关键)
let diff = index - centerIdx
if (diff > 2) diff -= 5
if (diff < -2) diff += 5
const cardPos = diff + 2
// 插值计算
let pos
if (Number.isInteger(cardPos)) {
pos = positions[cardPos] ?? positions[2]
} else {
const prev = Math.floor(cardPos)
const next = (prev + 1) % 5
const t = cardPos - prev
const p = positions[prev] ?? positions[2]
const n = positions[next] ?? positions[2]
pos = {
left: p.left + (n.left - p.left) * t,
top: p.top + (n.top - p.top) * t,
rotate: p.rotate + (n.rotate - p.rotate) * t,
scale: p.scale + (n.scale - p.scale) * t,
}
}
// ====================== ZINDEX 终极修复 ======================
const distance = Math.abs(cardPos - 2)
const zIndex = Math.round(100 - distance * 30) // 越靠近中间层级越高
const isCenter = distance < 0.02
if (isCenter) {
return {
left: pos.left + 'rpx',
top: pos.top + 'rpx',
transform: `scale(${pos.scale * 1.15}) rotate(${pos.rotate}deg)`,
zIndex: 999, // 中心永远最高
}
}
return {
left: pos.left + 'rpx',
top: pos.top + 'rpx',
transform: `scale(${pos.scale}) rotate(${pos.rotate}deg)`,
zIndex,
}
},
getCardStackPosition(index) {
const progress = -this.dragOffset / this.SWIPE_STEP
const centerIdx = this.selectedIndex + progress
let diff = index - centerIdx
if (diff > 2) diff -= 5
if (diff < -2) diff += 5
return diff + 2
},
onCardFrameTap(index) {
const card = this.currentCardList[index]
if (!card) return
const pos = this.getCardStackPosition(index)
if (Math.abs(pos - 2) < 0.2) {
if (card.name === '光栅卡') {
uni.navigateTo({
url: this.cardRoutes['光栅卡'] + '?name=' + encodeURIComponent(card.name),
})
} else {
uni.showToast({ title: '激情开发中', icon: 'none' })
}
return
}
this.selectedIndex = index
},
scrollUp() {
if (this.selectedCategoryIndex > 0) this.selectCategory(this.selectedCategoryIndex - 1)
},
scrollDown() {
if (this.selectedCategoryIndex < this.categoryList.length - 1) {
this.selectCategory(this.selectedCategoryIndex + 1)
}
},
},
}
</script>
<style lang="scss" scoped>
.page {
height: 100vh;
position: relative;
overflow: hidden;
}
.bg-wrapper {
position: fixed;
top: -16rpx;
left: 0;
width: 100%;
height: 110%;
z-index: 0;
}
.main-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
z-index: 10;
}
.cards-container {
flex: 1;
position: relative;
}
.card-item {
position: absolute;
width: 344rpx;
height: 344rpx;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform, z-index;
}
.card-item.no-transition {
transition: none !important;
}
.card-frame {
position: relative;
width: 100%;
height: 100%;
border-radius: 20rpx;
padding: 10rpx;
background-image: url('/static/square/cangpinkuang1.png');
background-size: cover;
}
.card-frame.no-border {
background-image: none;
}
.card-image {
width: 100%;
height: 100%;
border-radius: 14rpx;
object-fit: cover;
}
.coming-soon-badge {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 160rpx;
height: 160rpx;
}
.text-panel {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 392rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: url('/static/castlove/xiahualan.png') no-repeat center;
background-size: 130%;
border-radius: 20rpx;
}
.arrow-btn {
width: 60rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.8;
}
.arrow-icon {
width: 48rpx;
height: 48rpx;
}
.text-list {
display: flex;
flex-direction: column;
padding: 0 20rpx;
}
.text-item {
color: #fff;
font-size: 26rpx;
font-weight: 500;
padding: 10rpx 20rpx;
border-radius: 14rpx;
display: flex;
justify-content: center;
}
.text-item.active {
font-weight: bold;
background: url('/static/nft/dingbutubiao_liang.png') no-repeat center;
background-size: 100% 100%;
}
.font-large {
font-size: 34rpx;
}
.font-mid {
font-size: 30rpx;
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80%;
padding: 40rpx 0;
pointer-events: none;
}
.empty-state-icon {
width: 200rpx;
height: 200rpx;
opacity: 0.85;
margin-bottom: 32rpx;
}
.empty-state-title {
color: #fff;
font-size: 36rpx;
font-weight: 600;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.35);
}
.empty-state-desc {
color: rgba(255, 255, 255, 0.7);
font-size: 26rpx;
text-align: center;
}
</style>