统一导览点位摘要卡视觉体验
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
cxk
2026-07-20 22:01:25 +08:00
parent 7ae539accc
commit 735c5cd1ee
4 changed files with 113 additions and 382 deletions

View File

@@ -0,0 +1,79 @@
<template>
<view class="guide-poi-card" :class="{ collapsed }">
<view v-if="collapsible" class="poi-card-handle" @tap.stop="emit('toggle')"></view>
<view v-if="collapsed" class="poi-card-collapsed" @tap="emit('expand')">
<view class="poi-card-copy">
<text class="poi-card-title">{{ title }}</text>
<text class="poi-card-subtitle">{{ subtitle }}</text>
</view>
<view class="poi-card-mini-action"><text class="poi-card-mini-text">展开</text></view>
</view>
<template v-else>
<view class="poi-card-header">
<view class="poi-card-copy">
<text class="poi-card-title">{{ title }}</text>
<text v-if="subtitle" class="poi-card-subtitle">{{ subtitle }}</text>
</view>
<view class="poi-card-header-actions">
<view v-if="collapsible" class="poi-card-soft-action" @tap="emit('collapse')"><text class="poi-card-soft-text">收起</text></view>
<view class="poi-card-close" :data-testid="closeTestId" aria-label="关闭地点信息" @tap.stop="emit('close')"><text class="poi-card-close-text">×</text></view>
</view>
</view>
<view v-if="metaItems.length" class="poi-card-meta" data-testid="facility-detail-meta">
<view v-for="item in metaItems" :key="item.label" class="poi-card-meta-item">
<text class="poi-card-meta-label">{{ item.label }}</text>
<text class="poi-card-meta-value">{{ item.value }}</text>
</view>
</view>
<text v-if="description" class="poi-card-description">{{ description }}</text>
<view v-if="errorMessage" class="poi-card-error" data-testid="facility-location-error"><text class="poi-card-error-text">{{ errorMessage }}</text></view>
<view v-if="actionText" class="poi-card-actions"><view class="poi-action primary" @tap="emit('action')"><text class="poi-action-text">{{ actionText }}</text></view></view>
</template>
</view>
</template>
<script setup lang="ts">
interface GuidePoiSummaryMetaItem { label: string; value: string }
withDefaults(defineProps<{
title: string
subtitle?: string
collapsed?: boolean
collapsible?: boolean
metaItems?: GuidePoiSummaryMetaItem[]
description?: string
errorMessage?: string
actionText?: string
closeTestId?: string
}>(), {
subtitle: '', collapsed: false, collapsible: false, metaItems: () => [], description: '', errorMessage: '', actionText: '', closeTestId: ''
})
const emit = defineEmits<{ close: []; collapse: []; expand: []; toggle: []; action: [] }>()
</script>
<style scoped lang="scss">
.guide-poi-card { position: absolute; left: 12px; right: 12px; bottom: calc(env(safe-area-inset-bottom) + 24px); box-sizing: border-box; padding: 10px 14px 14px; background: rgba(255, 255, 255, 0.96); border: 1px solid #e5e6de; border-radius: 8px; box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12); z-index: 1003; }
.guide-poi-card.collapsed { padding: 8px 10px 10px; }
.poi-card-handle { width: 38px; height: 4px; margin: 0 auto 10px; background: #d8dbd2; border-radius: 999px; }
.poi-card-collapsed, .poi-card-header { display: flex; justify-content: space-between; gap: 10px; }
.poi-card-collapsed { min-height: 44px; align-items: center; gap: 12px; }
.poi-card-header { align-items: flex-start; }
.poi-card-copy { min-width: 0; display: flex; flex: 1; flex-direction: column; gap: 3px; }
.poi-card-title { overflow: hidden; color: #151713; font-size: 17px; font-weight: 700; line-height: 24px; text-overflow: ellipsis; white-space: nowrap; }
.poi-card-subtitle, .poi-card-meta-label { color: #696962; font-size: 12px; line-height: 17px; }
.poi-card-header-actions { display: flex; flex: 0 0 auto; gap: 6px; }
.poi-card-soft-action, .poi-card-close, .poi-card-mini-action { display: flex; align-items: center; justify-content: center; border-radius: 8px; }
.poi-card-soft-action, .poi-card-close { height: 28px; box-sizing: border-box; background: #f5f7f2; border: 1px solid #e4e5df; }
.poi-card-soft-action { padding: 0 10px; }.poi-card-close { width: 28px; }.poi-card-soft-text { color: #545861; font-size: 12px; font-weight: 500; line-height: 16px; }.poi-card-close-text { color: #545861; font-size: 18px; line-height: 20px; }
.poi-card-mini-action { height: 30px; padding: 0 10px; background: #151713; }.poi-card-mini-text { color: var(--museum-accent); font-size: 12px; font-weight: 500; line-height: 16px; }
.poi-card-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 12px; }
.poi-card-meta-item { display: flex; min-width: 0; flex-direction: column; gap: 2px; padding: 8px 10px; box-sizing: border-box; background: #f7f8f3; border: 1px solid #e5e6de; border-radius: 8px; }
.poi-card-meta-value { overflow: hidden; color: #1f2329; font-size: 14px; font-weight: 600; line-height: 20px; text-overflow: ellipsis; white-space: nowrap; }
.poi-card-description { display: block; margin-top: 10px; color: #424754; font-size: 13px; line-height: 19px; }.poi-card-error { margin-top: 10px; padding: 9px 10px; background: #fff4e8; border: 1px solid #f1c99e; border-radius: 8px; }.poi-card-error-text { color: #7a3e05; font-size: 13px; line-height: 18px; }
.poi-card-actions { display: grid; grid-template-columns: minmax(0, 1fr); margin-top: 12px; }.poi-action { display: flex; align-items: center; justify-content: center; min-width: 0; height: 38px; padding: 0 16px; box-sizing: border-box; border: 1px solid #151713; border-radius: 8px; background: #151713; }.poi-action-text { color: var(--museum-accent); font-size: 12px; font-weight: 500; line-height: 16px; text-align: center; }
@media (max-width: 360px) { .poi-card-meta { gap: 8px; } }
</style>

View File

@@ -11,7 +11,7 @@
v-if="mapContextReady" v-if="mapContextReady"
:search-text="facility.name" :search-text="facility.name"
search-top="20px" search-top="20px"
floor-bottom="153px" floor-bottom="280px"
floor-max-height="206px" floor-max-height="206px"
floor-side="left" floor-side="left"
:active-mode="activeMode" :active-mode="activeMode"
@@ -27,49 +27,21 @@
:show-mode-row="false" :show-mode-row="false"
show-floor-header show-floor-header
show-zoom-controls show-zoom-controls
zoom-controls-top="calc(100vh - 252px)" zoom-controls-top="calc(100vh - 360px)"
@floor-change="handleFloorChange" @floor-change="handleFloorChange"
@target-focus="handleTargetFocus" @target-focus="handleTargetFocus"
> >
<view class="detail-sheet" data-testid="facility-detail-sheet"> <GuidePoiSummaryCard
<view class="detail-title-row"> class="detail-sheet"
<view class="target-dot"></view> data-testid="facility-detail-sheet"
<view class="detail-title-copy"> :title="facility.name"
<text class="detail-title">{{ facility.name }}</text> :subtitle="`${facility.floor} · ${facility.category}`"
</view> :meta-items="facilitySummaryMetaItems"
<view :description="facility.description"
class="detail-close" :error-message="locationErrorMessage"
data-testid="facility-detail-close" close-test-id="facility-detail-close"
aria-label="关闭点位详情" @close="handleCloseDetailPanel"
@tap.stop="handleCloseDetailPanel" />
>
<text class="detail-close-text">×</text>
</view>
</view>
<view class="detail-meta" data-testid="facility-detail-meta">
<view class="detail-meta-item">
<text class="detail-meta-label">类别</text>
<text class="detail-meta-value">{{ facility.category }}</text>
</view>
<view class="detail-meta-item">
<text class="detail-meta-label">楼层</text>
<text class="detail-meta-value">{{ facility.floor }}</text>
</view>
</view>
<text v-if="facility.description" class="detail-description">
{{ facility.description }}
</text>
<view
v-if="locationErrorMessage"
class="detail-location-error"
data-testid="facility-location-error"
>
<text class="detail-location-error-text">{{ locationErrorMessage }}</text>
</view>
</view>
</GuideMapShell> </GuideMapShell>
<view v-else class="detail-map-loading" data-testid="facility-map-loading"> <view v-else class="detail-map-loading" data-testid="facility-map-loading">
<text class="detail-map-loading-text">正在准备位置预览...</text> <text class="detail-map-loading-text">正在准备位置预览...</text>
@@ -78,10 +50,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue' import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import GuideMapShell from '@/components/navigation/GuideMapShell.vue' import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
import GuidePoiSummaryCard from '@/components/map/GuidePoiSummaryCard.vue'
import { import {
guideUseCase guideUseCase
} from '@/usecases/guideUseCase' } from '@/usecases/guideUseCase'
@@ -138,6 +111,11 @@ const facility = ref<FacilityDetailViewModel>({
description: '' description: ''
}) })
const facilitySummaryMetaItems = computed(() => ([
{ label: '类别', value: facility.value.category },
{ label: '楼层', value: facility.value.floor }
]))
const searchContext = ref<FacilityDetailSearchContext>( const searchContext = ref<FacilityDetailSearchContext>(
parseFacilityDetailSearchContext() parseFacilityDetailSearchContext()
) )
@@ -448,143 +426,4 @@ const handlePageBack = () => {
font-size: 14px; font-size: 14px;
} }
.detail-sheet {
position: absolute;
left: 84px;
right: 16px;
bottom: calc(env(safe-area-inset-bottom) + 24px);
min-height: 110px;
padding: 14px 14px 12px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.96);
border: 1px solid #e5e6de;
border-radius: 8px;
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
z-index: 45;
}
.detail-title-row {
display: flex;
align-items: flex-start;
gap: 10px;
}
.target-dot {
width: 14px;
height: 14px;
margin-top: 5px;
flex-shrink: 0;
box-sizing: border-box;
background: var(--museum-accent);
border: 3px solid #151713;
border-radius: 999px;
box-shadow: 0 0 0 4px rgba(224, 225, 0, 0.18);
}
.detail-title-copy {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.detail-close {
width: 28px;
height: 28px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
background: #f4f5ef;
border: 1px solid #dde5df;
border-radius: 8px;
}
.detail-close-text {
font-size: 18px;
line-height: 20px;
font-weight: 500;
color: #30352f;
}
.detail-title {
font-size: 17px;
line-height: 24px;
font-weight: 700;
color: #151713;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.detail-meta {
margin-top: 12px;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 10px;
}
.detail-meta-item {
min-width: 0;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 2px;
box-sizing: border-box;
background: #f7f8f3;
border: 1px solid #e5e6de;
border-radius: 8px;
}
.detail-meta-label {
font-size: 12px;
line-height: 16px;
color: #696962;
}
.detail-meta-value {
min-width: 0;
font-size: 14px;
line-height: 20px;
font-weight: 600;
color: #1f2329;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.detail-description {
display: block;
margin-top: 10px;
font-size: 13px;
line-height: 19px;
color: #424754;
}
.detail-location-error {
margin-top: 10px;
padding: 9px 10px;
background: #fff4e8;
border: 1px solid #f1c99e;
border-radius: 8px;
}
.detail-location-error-text {
font-size: 13px;
line-height: 18px;
color: #7a3e05;
}
@media (max-width: 360px) {
.detail-sheet {
left: 78px;
right: 12px;
}
.detail-meta {
gap: 8px;
}
}
</style> </style>

View File

@@ -68,46 +68,19 @@
</view> </view>
</template> </template>
<view <GuidePoiSummaryCard
v-if="selectedGuidePoi && !showRoutePlanner && !isSimulatingRoute" v-if="selectedGuidePoi && !showRoutePlanner && !isSimulatingRoute"
class="guide-poi-card" :title="selectedGuidePoi.name"
:class="{ collapsed: isPoiCardCollapsed }" :subtitle="isPoiCardCollapsed ? selectedGuidePoiCollapsedSubtitle : selectedGuidePoiSubtitle"
> :collapsed="isPoiCardCollapsed"
<view class="poi-card-handle" @tap.stop="togglePoiCardCollapsed"></view> collapsible
<view v-if="isPoiCardCollapsed" class="poi-card-collapsed" @tap="expandPoiCard"> :action-text="selectedGuidePoiDetailTarget ? selectedGuidePoiDetailActionText : ''"
<view class="poi-card-copy"> @toggle="togglePoiCardCollapsed"
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text> @expand="expandPoiCard"
<text class="poi-card-subtitle">{{ selectedGuidePoiCollapsedSubtitle }}</text> @collapse="collapsePoiCard"
</view> @close="handleGuidePoiCardClose"
<view class="poi-card-mini-action"> @action="handleViewSelectedPoiDetail"
<text class="poi-card-mini-text">展开</text> />
</view>
</view>
<template v-else>
<view class="poi-card-header">
<view class="poi-card-copy">
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiSubtitle }}</text>
</view>
<view class="poi-card-header-actions">
<view class="poi-card-soft-action" @tap="collapsePoiCard">
<text class="poi-card-soft-text">收起</text>
</view>
<view class="poi-card-close" @tap="handleGuidePoiCardClose">
<text class="poi-card-close-text">×</text>
</view>
</view>
</view>
<view
v-if="selectedGuidePoiDetailTarget"
class="poi-card-actions"
>
<view class="poi-action primary" @tap="handleViewSelectedPoiDetail">
<text class="poi-action-text">{{ selectedGuidePoiDetailActionText }}</text>
</view>
</view>
</template>
</view>
<view <view
v-if="showGuideFloatingActions" v-if="showGuideFloatingActions"
@@ -291,6 +264,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue' import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import GuideMapShell from '@/components/navigation/GuideMapShell.vue' import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
import GuidePoiSummaryCard from '@/components/map/GuidePoiSummaryCard.vue'
import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue' import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue' import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue'
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue' import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
@@ -1816,168 +1790,6 @@ const handleExplainBack = () => {
z-index: 10; z-index: 10;
} }
.guide-poi-card {
position: absolute;
left: 12px;
right: 12px;
bottom: calc(env(safe-area-inset-bottom) + 24px);
box-sizing: border-box;
background: rgba(255, 255, 255, 0.96);
border: 1px solid #e5e6de;
border-radius: 8px;
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
z-index: 1003;
}
.guide-poi-card {
padding: 10px 14px 14px;
}
.guide-poi-card.collapsed {
padding: 8px 10px 10px;
}
.poi-card-handle {
width: 38px;
height: 4px;
margin: 0 auto 10px;
background: #d8dbd2;
border-radius: 999px;
}
.poi-card-collapsed {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.poi-card-mini-action {
flex: 0 0 auto;
height: 30px;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: center;
background: #151713;
border-radius: 8px;
}
.poi-card-mini-text {
font-size: 12px;
line-height: 16px;
font-weight: 500;
color: var(--museum-accent);
}
.poi-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.poi-card-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.poi-card-title {
font-size: 17px;
line-height: 24px;
font-weight: 700;
color: #151713;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.poi-card-subtitle {
font-size: 12px;
line-height: 17px;
color: #696962;
}
.poi-card-header-actions {
flex: 0 0 auto;
display: flex;
gap: 6px;
}
.poi-card-soft-action {
height: 28px;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1px solid #e4e5df;
border-radius: 8px;
background: #f5f7f2;
}
.poi-card-soft-text {
font-size: 12px;
line-height: 16px;
font-weight: 500;
color: #545861;
}
.poi-card-close {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #f5f7f2;
}
.poi-card-close-text {
font-size: 18px;
line-height: 20px;
color: #545861;
}
.poi-card-actions {
margin-top: 12px;
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.poi-action {
min-width: 0;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
box-sizing: border-box;
background: #f6f7f4;
border: 1px solid #e5e6de;
border-radius: 8px;
}
.poi-action.primary {
background: #151713;
border-color: #151713;
}
.poi-action-text {
font-size: 12px;
line-height: 16px;
font-weight: 500;
color: #151713;
text-align: center;
}
.poi-action.primary .poi-action-text {
color: var(--museum-accent);
}
.guide-home-dock { .guide-home-dock {
position: absolute; position: absolute;
--museum-h5-page-width: min(100vw, 430px); --museum-h5-page-width: min(100vw, 430px);

View File

@@ -173,6 +173,7 @@ describe('点位详情实际渲染与返回闭环', () => {
}) })
const sheet = wrapper.get('[data-testid="facility-detail-sheet"]') const sheet = wrapper.get('[data-testid="facility-detail-sheet"]')
expect(sheet.classes()).toContain('guide-poi-card')
expect(sheet.text()).toContain('二层卫生间') expect(sheet.text()).toContain('二层卫生间')
expect(sheet.text()).toContain('类别洗手间') expect(sheet.text()).toContain('类别洗手间')
expect(sheet.text()).toContain('楼层2F') expect(sheet.text()).toContain('楼层2F')