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

636 lines
17 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>
<text class="card-name">{{ card.name }}</text>
</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/leisheka.png",
comingSoon: false,
},
{
name: "拍立得",
image: "/static/castlove/pailide.png",
comingSoon: false,
},
{
name: "开发中",
image: "/static/castlove/daikaifa.png",
comingSoon: true,
},
{
name: "撕拉片",
image: "/static/castlove/silapian.png",
comingSoon: false,
},
],
吧唧: [
{
name: "超复古",
image: "/static/castlove/fugu.png",
comingSoon: false,
},
{
name: "卡通刺绣",
image: "/static/castlove/katongchixiu.png",
comingSoon: false,
},
{
name: "云母片",
image: "/static/castlove/yunmupian.png",
comingSoon: false,
},
{
name: "开发中",
image: "/static/castlove/daikaifa.png",
comingSoon: true,
},
],
海报: [
{
name: "拼豆",
image: "/static/castlove/pindou.png",
comingSoon: false,
},
{
name: "简繁插画",
image: "/static/castlove/jinfanchahua.png",
comingSoon: false,
},
{
name: "街头拼贴",
image: "/static/castlove/jietoupintie.png",
comingSoon: false,
},
{
name: "开发中",
image: "/static/castlove/daikaifa.png",
comingSoon: true,
},
],
},
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;
},
// 当前分类下的小类型卡片数量
// (与 cardListMap 中该类型数组的 length 一一对应,后续新增类型无需修改此处)
currentCardCount() {
return this.currentCardList.length;
},
// 扇形中心索引(取整)
// 奇数 N:正中;偶数 N:稍偏下的中间位
currentCenterIndex() {
return Math.floor(this.currentCardCount / 2);
},
},
methods: {
// 根据 type 字符串定位到对应分类索引
applyType(type) {
if (!type) return;
const idx = this.categoryTypeMap[type];
if (typeof idx === "number") {
this.selectedCategoryIndex = idx;
}
},
selectCategory(index) {
this.selectedCategoryIndex = index;
// 切换分类后,根据新分类的卡片数量重置中心索引
// (直接读 cardListMap 避免依赖 computed 在同 tick 内的更新)
const newCount =
this.cardListMap[this.categoryList[index]?.name]?.length || 0;
this.selectedIndex = Math.floor(newCount / 2);
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 N = this.currentCardCount;
if (N === 0) return;
const moveCount = Math.round(-this.dragOffset / this.SWIPE_STEP);
let newIdx = this.selectedIndex + moveCount;
newIdx = ((newIdx % N) + N) % N;
this.selectedIndex = newIdx;
this.dragOffset = 0;
},
// ====================== 核心修复z-index + 层级 + 循环 ======================
// 返回当前分类对应的扇形位置
// 规则:基础 5 个位置保持原样不动
// - N <= 5:取前 N 个
// - N > 5:基础 5 个 + (N-5) 个在末尾沿用底部样式追加
// 这样新增/删减小类型卡片时,既有的位置不会被打乱重组
getStackPositions(count) {
if (count <= 0) return [];
// 基础 5 张卡片的扇形布局(保持原状)
const basePositions = [
{ 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 },
];
if (count <= basePositions.length) {
// 卡片少于 5 张:直接取前 N 个,不动基础数组
return basePositions.slice(0, count);
}
// 卡片多于 5 张:在末尾追加,沿用第 5 张的样式继续向下延伸
const positions = [...basePositions];
const tail = basePositions[basePositions.length - 1];
for (let i = basePositions.length; i < count; i++) {
const extra = i - basePositions.length + 1;
positions.push({
left: tail.left,
top: tail.top + extra * 208, // 沿用 888→1096 的间距
rotate: tail.rotate - extra * 12, // 沿用 -12°→-25° 的旋转步长
scale: tail.scale,
});
}
return positions;
},
getCardStyle(index) {
const N = this.currentCardCount;
if (N === 0) return {};
const positions = this.getStackPositions(N);
const centerInt = this.currentCenterIndex; // 扇形中心索引(N=5→2;N=4→2;N=6→3)
const centerPos = positions[centerInt] ?? positions[0];
const progress = -this.dragOffset / this.SWIPE_STEP;
const centerIdx = this.selectedIndex + progress;
// 循环最短差值(关键):以 N/2 作为环绕阈值,适配任意 N
let diff = index - centerIdx;
const half = N / 2;
if (diff > half) diff -= N;
if (diff < -half) diff += N;
const cardPos = diff + centerInt;
// 插值计算(支持首尾环绕)
let pos;
const lowerRaw = Math.floor(cardPos);
const t = cardPos - lowerRaw;
const lowerIdx = ((lowerRaw % N) + N) % N;
const upperIdx = (lowerIdx + 1) % N;
if (t === 0 && lowerIdx >= 0 && lowerIdx < N) {
pos = positions[lowerIdx] ?? centerPos;
} else {
const p = positions[lowerIdx] ?? centerPos;
const n = positions[upperIdx] ?? centerPos;
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 - centerInt);
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 N = this.currentCardCount;
if (N === 0) return 0;
const centerInt = this.currentCenterIndex;
const centerIdx = this.selectedIndex + -this.dragOffset / this.SWIPE_STEP;
let diff = index - centerIdx;
const half = N / 2;
if (diff > half) diff -= N;
if (diff < -half) diff += N;
return diff + centerInt;
},
onCardFrameTap(index) {
const card = this.currentCardList[index];
if (!card) return;
const pos = this.getCardStackPosition(index);
if (Math.abs(pos - this.currentCenterIndex) < 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: 312rpx;
height: 312rpx;
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: 24rpx;
padding: 24rpx;
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: 24rpx;
object-fit: cover;
}
.coming-soon-badge {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 160rpx;
height: 160rpx;
}
.card-name {
position: absolute;
bottom: 0;
left: 32rpx;
color: #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
font-size: 32rpx;
font-weight: 400;
}
.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>