topfans/frontend/pages/castlove/craft-select.vue
2026-06-04 00:51:29 +08:00

782 lines
22 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);
},
},
},
computed: {
currentCardList() {
const categoryName = this.categoryList[this.selectedCategoryIndex].name;
return this.cardListMap[categoryName] || this.cardList;
},
},
methods: {
// 选择大分类
selectCategory(index) {
this.selectedCategoryIndex = index;
this.selectedIndex = 0; // 重置子选项选中
},
// 触摸开始
onTouchStart(e) {
this.touchStartY = e.touches[0].clientY;
},
// 触摸结束 - 滑动切换工艺卡片
onTouchEnd(e) {
const touchEndY = e.changedTouches[0].clientY;
const diff = this.touchStartY - touchEndY;
const threshold = 50; // 滑动阈值
if (diff > threshold) {
// 向上滑动 → 下一个工艺卡片
let newIndex = this.selectedIndex + 1;
if (newIndex < this.currentCardList.length) {
this.selectCard(newIndex);
}
} else if (diff < -threshold) {
// 向下滑动 → 上一个工艺卡片
let newIndex = this.selectedIndex - 1;
if (newIndex >= 0) {
this.selectCard(newIndex);
}
}
},
// 获取卡片样式 - 循环滚动布局
// positions 定义了5个位置的固定样式位置0最上位置2中间位置4最下
// 当选中某个卡片时该卡片显示在位置2中间其他卡片循环填充
getCardStyle(index) {
const positions = [
{ left: 9 * 32, top: 2 * 32, rotate: 25, scale: 0.75 }, // 位置0 - 最上
{ left: 3.75 * 32, top: 9 * 32, rotate: 12, scale: 0.95 }, // 位置1 - 中上
{ left: 60, top: 580, rotate: 0, scale: 1 }, // 位置2 - 中间
{ left: 3.75 * 32, top: 27.75 * 32, rotate: -12, scale: 0.95 }, // 位置3 - 中下
{ left: 7 * 32, top: 34.25 * 32, rotate: -25, scale: 0.75 }, // 位置4 - 最下
];
// 计算当前卡片应该显示在哪个位置
// 循环移位selectedIndex 的卡片显示在位置2中间
const cardPos = (index - this.selectedIndex + 2 + 5) % 5;
const pos = positions[cardPos];
// 选中卡片在中间位置时放大
if (cardPos === 2) {
return {
left: `${pos.left}rpx`,
top: `${pos.top}rpx`,
transform: `scale(${pos.scale * 1.15}) rotate(0deg)`,
zIndex: 100,
};
}
return {
left: `${pos.left}rpx`,
top: `${pos.top}rpx`,
transform: `scale(${pos.scale}) rotate(${pos.rotate}deg)`,
zIndex: 30 - Math.abs(cardPos - 2) * 10,
};
},
// 选择卡片
selectCard(index) {
this.selectedIndex = index;
},
/** 当前叠卡在弧形布局中的槽位2=正中主图最大1=中上「第二张」叠层0最上… */
getCardStackPosition(index) {
return (index - this.selectedIndex + 2 + 5) % 5;
},
/**
* 点击卡图区域:
* - 正中主图(槽位 2→ 进入对应工艺创建页(光栅卡即进入已接入预览的 create
* - 中上叠层(槽位 1常为拍立得示意在选中光栅时 → 同样进入光栅卡创建(与设计稿「点第二张进光栅」一致)
* - 其余叠层 → 仅切换选中
*/
onCardFrameTap(index) {
const card = this.currentCardList[index];
if (!card) {
return;
}
const pos = this.getCardStackPosition(index);
// 只有中间位置的卡片点击才会进入创建页
// 其他位置点击只是切换选中(把卡片移到中间),再次点击中间卡片才进入
if (pos === 2) {
if (card.name === "光栅卡" || card.name === "镭射卡") {
const route = this.cardRoutes[card.name];
if (route) {
uni.navigateTo({
url: `${route}?name=${encodeURIComponent(card.name)}`,
});
}
} else {
uni.showToast({
title: "激情开发中",
icon: "none",
});
}
return;
}
this.selectCard(index);
},
handleBack() {
uni.navigateBack();
},
scrollUp() {
let newIndex = this.selectedCategoryIndex - 1;
if (newIndex >= 0) {
this.selectCategory(newIndex);
}
},
scrollDown() {
let newIndex = this.selectedCategoryIndex + 1;
if (newIndex < this.categoryList.length) {
this.selectCategory(newIndex);
}
},
handleSkip() {
const card = this.cardList[this.selectedIndex];
if (card.name === "光栅卡" || card.name === "镭射卡") {
const route = this.cardRoutes[card.name];
if (route) {
uni.navigateTo({
url: `${route}?name=${encodeURIComponent(card.name)}`,
});
}
} else {
uni.showToast({
title: "激情开发中",
icon: "none",
});
}
},
},
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/leisheka.png",
comingSoon: false,
},
{
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/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);
},
// 各分类的初始 selectedIndex
// 与 data() 中的初始值语义保持一致,统一为"默认第 2 张"
// 后续如需让每个分类回到"各自的中心",可在此处按分类返回不同值
defaultSelectedIndex() {
return 1;
},
},
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 = this.defaultSelectedIndex;
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 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: -16rpx;
left: 32rpx;
background: linear-gradient(
93.1deg,
rgba(224, 180, 247, 0.71) -12.06%,
rgba(178, 246, 204, 0.71) 52.09%,
rgba(98, 178, 244, 0.71) 163.5%
);
backdrop-filter: blur(11.699999809265137px);
color: #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
font-size: 28rpx;
font-weight: 400;
padding: 0 16rpx;
border-radius: 16rpx;
}
.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>