1、王府井增加图片浏览组件
2、隆福寺增加指定区域点击及图片浏览
3、东直门增加桌子、炉子和烤鸭拖动
This commit is contained in:
Wenzhe 2026-01-31 12:37:54 +08:00
parent bca7267c03
commit 9c83d6a588
17 changed files with 620 additions and 2 deletions

View File

@ -39,6 +39,124 @@ const handleFuClick = () => {
emit('collect-seal') emit('collect-seal')
} }
//
const isDragging = ref(false)
const showDuck = ref(false)
const deskImage = ref('/static/dzm/img_desk1.png')
const showGuideElements = ref(true)
//
const duckElement = ref(null)
// 使
let duckX = 0
let duckY = 0
// (540, 1781, 100, 100)
const dragStartArea = { x: 590, y: 1831 }
// (: 13, 1665, 441, 382)
const targetArea = { x: 100, y: 1750, width: 300, height: 200 }
//
const startDrag = (e) => {
e.preventDefault()
//
if (isDragging.value) {
endDrag()
}
isDragging.value = true
showDuck.value = true
//
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
//
updateDuckPosition(clientX, clientY)
//
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', endDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', endDrag)
//
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', endDrag)
document.addEventListener('touchmove', onDrag, { passive: false })
document.addEventListener('touchend', endDrag)
}
//
const onDrag = (e) => {
if (!isDragging.value) return
e.preventDefault()
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
updateDuckPosition(clientX, clientY)
}
// - DOM
const updateDuckPosition = (clientX, clientY) => {
duckX = clientX
duckY = clientY
if (duckElement.value) {
// 使 transform3d GPU
duckElement.value.style.transform = `translate3d(${clientX}px, ${clientY}px, 0) translate(-50%, -50%) scale(1.2)`
}
}
//
const endDrag = () => {
if (!isDragging.value) return
//
const container = document.querySelector('.dongzhimen-scene-container')
if (container) {
const rect = container.getBoundingClientRect()
//
const duckRelX = duckX - rect.left
const duckRelY = duckY - rect.top
// rpx px (稿 750rpx rect.width )
const rpxToPx = rect.width / 750
const targetX = targetArea.x * rpxToPx
const targetY = targetArea.y * rpxToPx
const targetW = targetArea.width * rpxToPx
const targetH = targetArea.height * rpxToPx
const inTargetArea = (
duckRelX >= targetX &&
duckRelX <= targetX + targetW &&
duckRelY >= targetY &&
duckRelY <= targetY + targetH
)
if (inTargetArea) {
//
deskImage.value = '/static/dzm/img_desk2.png'
//
showGuideElements.value = false
}
}
//
showDuck.value = false
isDragging.value = false
//
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', endDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', endDrag)
}
// //
onMounted(() => { onMounted(() => {
// //
@ -75,6 +193,29 @@ onMounted(() => {
alt="新春祝福" alt="新春祝福"
class="sq-image" class="sq-image"
/> />
<!-- 装饰图片 -->
<img :src="deskImage" alt="餐桌" class="deco-img desk-img" />
<img src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" />
<img v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" />
<img v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" />
<!-- 拖拽触发区域 -->
<div
v-if="showGuideElements"
class="drag-trigger-area"
@mousedown="startDrag"
@touchstart="startDrag"
></div>
<!-- 跟随拖拽的鸭子图片 -->
<img
v-show="showDuck"
src="/static/dzm/img_duck.png"
alt="烤鸭"
class="drag-duck"
ref="duckElement"
/>
</section> </section>
</template> </template>
@ -319,6 +460,82 @@ onMounted(() => {
animation: fadeIn 0.5s ease; animation: fadeIn 0.5s ease;
} }
/* 装饰图片 */
.deco-img {
position: absolute;
z-index: 25;
}
.desk-img {
left: 13rpx;
top: 1665rpx;
width: 441rpx;
height: 382rpx;
}
.stove-img {
left: 492rpx;
top: 1711rpx;
width: 241rpx;
height: 363rpx;
}
.line-img {
left: 250rpx;
top: 1842rpx;
width: 360rpx;
height: 80rpx;
}
.hand-img {
left: 440rpx;
top: 1900rpx;
width: 38rpx;
height: 40rpx;
animation: arcSlideLeft 1.2s ease-in-out infinite;
}
/* 向左弧形滑动动效 */
@keyframes arcSlideLeft {
0% {
transform: translateX(0) translateY(0);
opacity: 1;
}
100% {
transform: translateX(-80rpx) translateY(3rpx);
opacity: 1;
}
}
/* 拖拽触发区域 */
.drag-trigger-area {
position: absolute;
left: 540rpx;
top: 1781rpx;
width: 100rpx;
height: 100rpx;
cursor: grab;
z-index: 30;
}
.drag-trigger-area:active {
cursor: grabbing;
}
/* 跟随拖拽的鸭子图片 */
.drag-duck {
position: fixed;
left: 0;
top: 0;
width: 54rpx;
height: 105rpx;
opacity: 0.8;
pointer-events: none;
z-index: 1000;
will-change: transform;
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 640px) { @media (max-width: 640px) {
.fu-word { .fu-word {

View File

@ -0,0 +1,208 @@
<script setup>
import { ref, watch } from 'vue'
//
const props = defineProps({
//
visible: {
type: Boolean,
default: false
},
//
images: {
type: Array,
default: () => []
},
//
currentIndex: {
type: Number,
default: 0
}
})
//
const emit = defineEmits(['close', 'update:currentIndex'])
//
const currentImageIndex = ref(props.currentIndex)
// props
watch(() => props.currentIndex, (newVal) => {
currentImageIndex.value = newVal
})
watch(() => props.visible, (newVal) => {
if (newVal) {
currentImageIndex.value = props.currentIndex
}
})
//
const prevImage = () => {
currentImageIndex.value = (currentImageIndex.value - 1 + props.images.length) % props.images.length
}
const nextImage = () => {
currentImageIndex.value = (currentImageIndex.value + 1) % props.images.length
}
//
const closeModal = () => {
emit('close')
}
//
const handleOverlayClick = () => {
closeModal()
}
</script>
<template>
<div class="gallery-modal" v-if="visible">
<!-- 遮罩层 -->
<div class="modal-overlay" @click="handleOverlayClick"></div>
<!-- 弹窗内容 -->
<div class="modal-content">
<!-- 图片区域 -->
<div class="gallery-image-wrapper">
<img :src="images[currentImageIndex]?.src" :alt="images[currentImageIndex]?.title" class="gallery-image" />
</div>
<!-- 控制区域 -->
<div class="gallery-controls">
<div class="nav-btn prev-btn" @click="prevImage">
<img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" />
</div>
<div class="gallery-title">{{ images[currentImageIndex]?.title }}</div>
<div class="nav-btn next-btn" @click="nextImage">
<img src="/static/images/btn_next.png" alt="下一张" class="nav-icon" />
</div>
</div>
</div>
<!-- 关闭按钮在对话框外边下方 -->
<div class="close-btn" @click="closeModal">
<img src="/static/images/btn_close.png" alt="关闭" class="close-icon" />
</div>
</div>
</template>
<style scoped>
/* 弹窗容器 */
.gallery-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* 遮罩层 */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
}
/* 弹窗内容 */
.modal-content {
position: relative;
width: 700rpx;
background-color: #d72717;
border-radius: 20rpx;
padding: 30rpx;
box-sizing: border-box;
z-index: 1001;
animation: modalIn 0.3s ease;
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 关闭按钮(在对话框外边下方) */
.close-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 1001;
margin-top: 30rpx;
}
.close-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 上方图片区域 */
.gallery-image-wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.gallery-image {
width: 100%;
height: auto;
max-height: 600rpx;
object-fit: contain;
border-radius: 12rpx;
display: block;
}
/* 下方控制区域 */
.gallery-controls {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
}
.nav-btn {
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
}
.nav-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
.gallery-title {
flex: 1;
text-align: center;
color: #fff;
font-size: 40rpx;
font-weight: 500;
letter-spacing: 6rpx;
padding: 0 20rpx;
}
</style>

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import FuClickArea from './FuClickArea.vue' import FuClickArea from './FuClickArea.vue'
import ImageGalleryModal from './ImageGalleryModal.vue'
// //
const props = defineProps({ const props = defineProps({
@ -39,6 +40,28 @@ const handleFuClick = () => {
emit('collect-seal') emit('collect-seal')
} }
//
const lfsImages = [
{ src: '/static/lfs/img1.png', title: '传艺承福阁' },
{ src: '/static/lfs/img2.png', title: '京味福食巷' },
{ src: '/static/lfs/img3.png', title: '雅趣福玩斋' }
]
//
const galleryVisible = ref(false)
const currentGalleryIndex = ref(0)
//
const openGallery = (index) => {
currentGalleryIndex.value = index
galleryVisible.value = true
}
//
const closeGallery = () => {
galleryVisible.value = false
}
// //
onMounted(() => { onMounted(() => {
// //
@ -76,6 +99,35 @@ onMounted(() => {
class="sq-image" class="sq-image"
/> />
<!-- 热点点击区域1 - 左下方 -->
<div class="hotspot-area" style="left: 163rpx; top: 950rpx;" @click="openGallery(0)">
<div class="pulse-indicator">
<div class="pulse-circle"></div>
</div>
</div>
<!-- 热点点击区域2 - 右下方 -->
<div class="hotspot-area" style="left: 450rpx; top: 900rpx;" @click="openGallery(1)">
<div class="pulse-indicator">
<div class="pulse-circle"></div>
</div>
</div>
<!-- 热点点击区域3 - 上方 -->
<div class="hotspot-area" style="left: 320rpx; top: 738rpx;" @click="openGallery(2)">
<div class="pulse-indicator">
<div class="pulse-circle"></div>
</div>
</div>
<!-- 图片浏览器弹窗 -->
<ImageGalleryModal
:visible="galleryVisible"
:images="lfsImages"
:currentIndex="currentGalleryIndex"
@close="closeGallery"
/>
</section> </section>
</template> </template>
@ -133,8 +185,8 @@ onMounted(() => {
.lantern { .lantern {
font-size: 2.5rem; font-size: 2.5rem;
animation: swing 3s infinite ease-in-out; animation: swing 3s infinite ease-in-out;
opacity: 0.9; opacity: 1;
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8)); filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.9));
color: #ffd700; color: #ffd700;
} }
@ -319,6 +371,49 @@ onMounted(() => {
animation: fadeIn 0.5s ease; animation: fadeIn 0.5s ease;
} }
/* 热点点击区域 */
.hotspot-area {
position: absolute;
width: 150rpx;
height: 150rpx;
cursor: pointer;
z-index: 25;
display: flex;
align-items: center;
justify-content: center;
}
/* 脉冲动效 */
.pulse-indicator {
position: relative;
width: 100rpx;
height: 100rpx;
}
.pulse-circle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(255, 215, 0, 0.4);
border: 3rpx solid rgba(255, 215, 0, 0.8);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0.8;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 640px) { @media (max-width: 640px) {
.fu-word { .fu-word {

View File

@ -39,6 +39,23 @@ const handleFuClick = () => {
emit('collect-seal') emit('collect-seal')
} }
//
const images = [
{ src: '/static/wfj/img1.png', title: '传艺承福阁' },
{ src: '/static/wfj/img2.png', title: '京味福食巷' },
{ src: '/static/wfj/img3.png', title: '雅趣福玩斋' }
]
const currentImageIndex = ref(0)
//
const prevImage = () => {
currentImageIndex.value = (currentImageIndex.value - 1 + images.length) % images.length
}
const nextImage = () => {
currentImageIndex.value = (currentImageIndex.value + 1) % images.length
}
// //
onMounted(() => { onMounted(() => {
// //
@ -76,6 +93,22 @@ onMounted(() => {
class="sq3-image" class="sq3-image"
/> />
<!-- 图片浏览组件 -->
<div class="image-gallery">
<div class="gallery-image-wrapper">
<img :src="images[currentImageIndex].src" :alt="images[currentImageIndex].title" class="gallery-image" />
</div>
<div class="gallery-controls">
<div class="nav-btn prev-btn" @click="prevImage">
<img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" />
</div>
<div class="gallery-title">{{ images[currentImageIndex].title }}</div>
<div class="nav-btn next-btn" @click="nextImage">
<img src="/static/images/btn_next.png" alt="下一张" class="nav-icon" />
</div>
</div>
</div>
</section> </section>
</template> </template>
@ -319,6 +352,71 @@ onMounted(() => {
animation: fadeIn 0.5s ease; animation: fadeIn 0.5s ease;
} }
/* 图片浏览组件 */
.image-gallery {
position: absolute;
left: 40rpx;
top: 1430rpx;
width: 670rpx;
background-color: #d72717;
border-radius: 20rpx;
padding: 20rpx;
box-sizing: border-box;
z-index: 30;
overflow: hidden;
}
/* 上方图片区域 */
.gallery-image-wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.gallery-image {
width: 100%;
height: auto;
object-fit: contain;
border-radius: 12rpx;
display: block;
}
/* 下方控制区域 */
.gallery-controls {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
}
.nav-btn {
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
}
.nav-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
.gallery-title {
flex: 1;
text-align: center;
color: #fff;
font-size: 40rpx;
font-weight: 500;
letter-spacing: 6rpx;
padding: 0 20rpx;
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 640px) { @media (max-width: 640px) {
.fu-word { .fu-word {

BIN
static/dzm/img_desk1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
static/dzm/img_desk2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
static/dzm/img_duck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
static/dzm/img_line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
static/dzm/img_stove1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
static/dzm/img_stove2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
static/images/btn_prev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
static/lfs/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
static/lfs/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

BIN
static/lfs/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
static/wfj/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
static/wfj/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

BIN
static/wfj/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB