修复导览与讲解位置预览逻辑
This commit is contained in:
@@ -5,39 +5,41 @@
|
||||
:show-top-tabs="false"
|
||||
show-bottom-nav
|
||||
show-back
|
||||
:show-cancel="isStartPanelOpen"
|
||||
@tab-change="handleTopTabChange"
|
||||
@back="handlePageBack"
|
||||
@cancel="handleCancelStartSelection"
|
||||
>
|
||||
<GuideMapShell
|
||||
:search-text="facility.name"
|
||||
search-top="60px"
|
||||
mode-top="104px"
|
||||
floor-top="208px"
|
||||
tools-top="176px"
|
||||
search-top="20px"
|
||||
floor-bottom="153px"
|
||||
floor-max-height="206px"
|
||||
floor-side="left"
|
||||
:active-mode="activeMode"
|
||||
:active-floor="activeFloor"
|
||||
:map-type="activeMode === '2d' ? 'outdoor' : 'indoor'"
|
||||
:tools="mapTools"
|
||||
map-type="indoor"
|
||||
:tools="[]"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
:target-focus-request="targetFocusRequest"
|
||||
:target-focus-distance-factor="0.86"
|
||||
:show-search="false"
|
||||
:show-mode-row="false"
|
||||
show-floor-header
|
||||
show-zoom-controls
|
||||
zoom-controls-top="calc(100vh - 252px)"
|
||||
@floor-change="handleFloorChange"
|
||||
@tool-click="handleToolClick"
|
||||
>
|
||||
<view class="detail-sheet">
|
||||
<view v-if="isDetailPanelVisible" class="detail-sheet">
|
||||
<view class="detail-title-row">
|
||||
<text class="detail-title">{{ facility.name }}</text>
|
||||
<text class="open-status">{{ facility.status }}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-lines">
|
||||
<text class="detail-line">所在区域:{{ facility.location }}</text>
|
||||
<text class="detail-line">能力说明:{{ facility.traffic }}</text>
|
||||
<text v-if="confirmedStart" class="detail-line">预览起点:{{ confirmedStart.label }}</text>
|
||||
<view class="target-dot"></view>
|
||||
<view class="detail-title-copy">
|
||||
<text class="detail-title">{{ facility.name }}</text>
|
||||
<text class="detail-subtitle">{{ facility.location }}</text>
|
||||
</view>
|
||||
<view class="detail-close" @tap.stop="handleCloseDetailPanel">
|
||||
<text class="detail-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tag-row">
|
||||
@@ -51,82 +53,35 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<view class="action-btn secondary" @tap="handleChooseStart">
|
||||
<text class="action-text">{{ startButtonLabel }}</text>
|
||||
</view>
|
||||
<view class="action-btn primary" @tap="handleStartNavigation">
|
||||
<text class="action-text">查看位置</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="detail-hint">已在对应楼层模型中标出点位</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="isStartPanelOpen"
|
||||
class="start-panel-mask"
|
||||
@tap="handleCancelStartSelection"
|
||||
></view>
|
||||
<view
|
||||
v-if="isStartPanelOpen"
|
||||
class="start-select-sheet"
|
||||
@tap.stop=""
|
||||
v-else
|
||||
class="detail-restore"
|
||||
@tap.stop="handleOpenDetailPanel"
|
||||
>
|
||||
<text class="start-select-title">选择预览起点</text>
|
||||
<text class="start-select-desc">当前支持选择预览起点,用于三维位置预览。</text>
|
||||
|
||||
<view class="start-chip-section">
|
||||
<text class="start-chip-label">楼层</text>
|
||||
<view class="start-chip-row">
|
||||
<view
|
||||
v-for="floor in startFloors"
|
||||
:key="floor"
|
||||
class="start-chip"
|
||||
:class="{ active: floor === selectedStartFloor }"
|
||||
@tap="handleStartFloorChange(floor)"
|
||||
>
|
||||
<text class="start-chip-text">{{ floor }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="start-chip-section area">
|
||||
<text class="start-chip-label">区域</text>
|
||||
<view class="start-chip-row">
|
||||
<view
|
||||
v-for="area in startAreas"
|
||||
:key="area"
|
||||
class="start-chip area"
|
||||
:class="{ active: area === selectedStartArea }"
|
||||
@tap="handleStartAreaChange(area)"
|
||||
>
|
||||
<text class="start-chip-text">{{ area }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="start-panel-actions">
|
||||
<view class="start-panel-btn secondary" @tap="handleCancelStartSelection">
|
||||
<text class="start-panel-btn-text">取消</text>
|
||||
</view>
|
||||
<view class="start-panel-btn primary" @tap="handleConfirmStartSelection">
|
||||
<text class="start-panel-btn-text">确认起点</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="target-dot small"></view>
|
||||
<text class="detail-restore-text">点位</text>
|
||||
</view>
|
||||
</GuideMapShell>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import {
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toFacilityDetailViewModel,
|
||||
toTargetFocusRequestViewModel,
|
||||
type TargetPoiFocusRequestViewModel,
|
||||
type FacilityDetailViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import {
|
||||
@@ -134,64 +89,51 @@ import {
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import type {
|
||||
MuseumFloor
|
||||
MuseumFloor,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
import type {
|
||||
GuideModelSource,
|
||||
GuideRenderPoi
|
||||
} from '@/domain/guideModel'
|
||||
import {
|
||||
StaticGuideModelRepository
|
||||
} from '@/repositories/GuideModelRepository'
|
||||
|
||||
interface ConfirmedStart {
|
||||
label: string
|
||||
floor: string
|
||||
source: 'facility-detail'
|
||||
}
|
||||
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const fallbackIndoorModelSource = new StaticGuideModelRepository()
|
||||
const indoorModelSource = ref<GuideModelSource>(fallbackIndoorModelSource)
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
const normalizeGuideFloorId = (labelOrId: string) =>
|
||||
guideFloors.value.find((floor) => floor.id === labelOrId || floor.label === labelOrId)?.id
|
||||
|| guideUseCase.normalizeFloorId(labelOrId)
|
||||
const activeMode = ref<'2d' | '3d'>('3d')
|
||||
const activeFloor = ref('1F')
|
||||
const targetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||||
const targetFocusRequestId = ref(0)
|
||||
const isDetailPanelVisible = ref(true)
|
||||
|
||||
const facility = ref<FacilityDetailViewModel>({
|
||||
id: '',
|
||||
name: '目标位置',
|
||||
status: '可预览',
|
||||
location: '中心线路网导览点位',
|
||||
location: '正在定位三维点位',
|
||||
traffic: guideUseCase.getRouteReadinessSnapshot().message,
|
||||
tags: ['三维位置', '中心线路网']
|
||||
tags: ['三维位置', '位置预览']
|
||||
})
|
||||
const isStartPanelOpen = ref(false)
|
||||
const selectedStartFloor = ref('1F')
|
||||
const selectedStartArea = ref('服务台附近')
|
||||
const confirmedStart = ref<ConfirmedStart | null>(null)
|
||||
const startFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||||
const mapTools = computed(() => ['重置', activeMode.value === '2d' ? '3D' : '2D'])
|
||||
const startAreas = ['主入口', '服务台附近', '停车场入口']
|
||||
|
||||
const pendingStartLabel = computed(() => `${selectedStartFloor.value} ${selectedStartArea.value}`)
|
||||
const startButtonLabel = computed(() => confirmedStart.value ? '更换起点' : '选择起点')
|
||||
|
||||
const resolveConfirmedStartArea = () => {
|
||||
if (!confirmedStart.value) return '服务台附近'
|
||||
|
||||
const area = confirmedStart.value.floor
|
||||
? confirmedStart.value.label.replace(`${confirmedStart.value.floor} `, '').trim()
|
||||
: confirmedStart.value.label.trim()
|
||||
|
||||
return startAreas.includes(area) ? area : '服务台附近'
|
||||
}
|
||||
|
||||
const syncDraftStartSelection = () => {
|
||||
selectedStartFloor.value = confirmedStart.value?.floor || '1F'
|
||||
selectedStartArea.value = resolveConfirmedStartArea()
|
||||
}
|
||||
|
||||
const loadGuideFloors = async () => {
|
||||
try {
|
||||
const floors = await guideUseCase.getFloors()
|
||||
guideFloors.value = floors
|
||||
const defaultFloor = floors.find((floor) => floor.label === selectedStartFloor.value)?.label
|
||||
const modelPackage = await fallbackIndoorModelSource.loadPackage()
|
||||
guideFloors.value = modelPackage.floors.map((floor) => ({
|
||||
id: floor.floorId,
|
||||
label: floor.label,
|
||||
order: floor.order
|
||||
}))
|
||||
const floors = guideFloors.value
|
||||
const defaultFloor = floors.find((floor) => floor.label === activeFloor.value)?.label
|
||||
|| floors.find((floor) => floor.id === 'L1')?.label
|
||||
|| floors[0]?.label
|
||||
|| selectedStartFloor.value
|
||||
selectedStartFloor.value = defaultFloor
|
||||
|| activeFloor.value
|
||||
activeFloor.value = floors.find((floor) => floor.label === activeFloor.value)?.label
|
||||
|| defaultFloor
|
||||
} catch (error) {
|
||||
@@ -200,8 +142,148 @@ const loadGuideFloors = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const focusFacilityPoi = (poi: MuseumPoi) => {
|
||||
if (!poi.floorId) return
|
||||
|
||||
activeMode.value = '3d'
|
||||
activeFloor.value = poi.floorLabel
|
||||
targetFocusRequestId.value += 1
|
||||
targetFocusRequest.value = toTargetFocusRequestViewModel({
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
primaryCategoryZh: poi.primaryCategory.label,
|
||||
positionGltf: poi.positionGltf,
|
||||
sourceObjectName: poi.sourceObjectName
|
||||
}, targetFocusRequestId.value)
|
||||
}
|
||||
|
||||
const focusRenderPoi = (poi: GuideRenderPoi) => {
|
||||
if (!poi.floorId) return
|
||||
|
||||
const floorLabel = guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
|
||||
activeMode.value = '3d'
|
||||
activeFloor.value = floorLabel
|
||||
targetFocusRequestId.value += 1
|
||||
targetFocusRequest.value = toTargetFocusRequestViewModel({
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel,
|
||||
primaryCategoryZh: poi.primaryCategoryZh,
|
||||
positionGltf: poi.positionGltf,
|
||||
sourceObjectName: poi.sourceObjectName,
|
||||
kind: poi.kind,
|
||||
hallId: poi.hallId,
|
||||
hallName: poi.hallName,
|
||||
entrances: poi.entrances
|
||||
}, targetFocusRequestId.value)
|
||||
}
|
||||
|
||||
const getRenderPoiFloorLabel = (poi: GuideRenderPoi) =>
|
||||
guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
|
||||
|
||||
const applyRenderPoiToFacility = (poi: GuideRenderPoi, routeMessage: string) => {
|
||||
const floorLabel = getRenderPoiFloorLabel(poi)
|
||||
const categoryLabel = poi.primaryCategoryZh || '三维位置'
|
||||
facility.value = {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
status: '可预览',
|
||||
location: `${floorLabel} · ${categoryLabel}`,
|
||||
traffic: routeMessage,
|
||||
tags: [
|
||||
floorLabel,
|
||||
categoryLabel,
|
||||
'展示点位'
|
||||
]
|
||||
}
|
||||
focusRenderPoi(poi)
|
||||
}
|
||||
|
||||
const normalizePoiName = (value: string) => value
|
||||
.trim()
|
||||
.replace(/[\s()()【】[\]_-]/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
const resolvePoiFromHall = async () => {
|
||||
if (!facility.value.id) return null
|
||||
|
||||
const hall = await explainUseCase.getHallById(facility.value.id).catch(() => null)
|
||||
const poiId = hall?.location?.poiId || hall?.poiId
|
||||
return poiId ? guideUseCase.getPoiById(poiId) : null
|
||||
}
|
||||
|
||||
const resolveFacilityPoi = async () => {
|
||||
const poiById = facility.value.id
|
||||
? await guideUseCase.getPoiById(facility.value.id)
|
||||
: null
|
||||
if (poiById) return poiById
|
||||
|
||||
const poiByHall = await resolvePoiFromHall()
|
||||
if (poiByHall) return poiByHall
|
||||
|
||||
const normalizedTarget = normalizePoiName(facility.value.name)
|
||||
if (!normalizedTarget) return null
|
||||
|
||||
const candidates = await guideUseCase.searchPois(facility.value.name)
|
||||
return candidates.find((candidate) => {
|
||||
const candidateName = normalizePoiName(candidate.name)
|
||||
const candidateHallName = normalizePoiName(candidate.hallName || '')
|
||||
return candidateName === normalizedTarget
|
||||
|| candidateHallName === normalizedTarget
|
||||
|| candidateName.includes(normalizedTarget)
|
||||
|| normalizedTarget.includes(candidateName)
|
||||
}) || candidates[0] || null
|
||||
}
|
||||
|
||||
const resolveRenderPoiByName = async () => {
|
||||
const normalizedTarget = normalizePoiName(facility.value.name)
|
||||
if (!normalizedTarget) return null
|
||||
|
||||
const modelPackage = await indoorModelSource.value.loadPackage()
|
||||
for (const floor of modelPackage.floors) {
|
||||
const floorId = floor.floorId
|
||||
const pois = await indoorModelSource.value.loadFloorPois(floorId).catch(() => [])
|
||||
const matchedPoi = pois.find((poi) => {
|
||||
const poiName = normalizePoiName(poi.name)
|
||||
const hallName = normalizePoiName(poi.hallName || '')
|
||||
const sourceObjectName = normalizePoiName(poi.sourceObjectName || '')
|
||||
return poiName === normalizedTarget
|
||||
|| hallName === normalizedTarget
|
||||
|| sourceObjectName === normalizedTarget
|
||||
|| poiName.includes(normalizedTarget)
|
||||
|| normalizedTarget.includes(poiName)
|
||||
|| sourceObjectName.includes(normalizedTarget)
|
||||
|| normalizedTarget.includes(sourceObjectName)
|
||||
})
|
||||
if (matchedPoi) return matchedPoi
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const tryResolveRenderPoiByName = async () => {
|
||||
try {
|
||||
return await resolveRenderPoiByName()
|
||||
} catch (error) {
|
||||
console.warn('当前模型源点位解析失败,尝试静态资源兜底:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const tryResolveFacilityPoi = async () => {
|
||||
try {
|
||||
return await resolveFacilityPoi()
|
||||
} catch (error) {
|
||||
console.warn('内容点位解析失败,继续使用模型点位预览:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
void loadGuideFloors()
|
||||
await loadGuideFloors()
|
||||
|
||||
if (options.id) {
|
||||
facility.value.id = options.id
|
||||
@@ -211,108 +293,41 @@ onLoad(async (options: any) => {
|
||||
facility.value.name = decodeURIComponent(options.target)
|
||||
}
|
||||
|
||||
if (!facility.value.id) return
|
||||
if (!facility.value.id && !normalizePoiName(facility.value.name)) return
|
||||
|
||||
try {
|
||||
const routeReadiness = await guideUseCase.getRouteReadiness()
|
||||
const poi = await guideUseCase.getPoiById(facility.value.id)
|
||||
if (!poi) return
|
||||
const routeReadinessMessage = guideUseCase.getRouteReadinessSnapshot().message
|
||||
|
||||
facility.value = toFacilityDetailViewModel(
|
||||
poi,
|
||||
routeReadiness.message
|
||||
)
|
||||
// 详情页以位置预览为主,优先使用模型点位,避免旧内容兜底覆盖真实三维点位。
|
||||
const renderPoi = await tryResolveRenderPoiByName()
|
||||
if (renderPoi) {
|
||||
applyRenderPoiToFacility(renderPoi, routeReadinessMessage)
|
||||
return
|
||||
}
|
||||
|
||||
const poi = await tryResolveFacilityPoi()
|
||||
if (poi) {
|
||||
facility.value = toFacilityDetailViewModel(
|
||||
poi,
|
||||
routeReadinessMessage
|
||||
)
|
||||
focusFacilityPoi(poi)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载中心线路网设施详情失败:', error)
|
||||
console.error('加载设施位置详情失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const handleSearchTap = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/index?keyword=${encodeURIComponent(facility.value.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleChooseStart = () => {
|
||||
syncDraftStartSelection()
|
||||
isStartPanelOpen.value = true
|
||||
}
|
||||
|
||||
const handleStartFloorChange = (floor: string) => {
|
||||
selectedStartFloor.value = floor
|
||||
}
|
||||
|
||||
const handleStartAreaChange = (area: string) => {
|
||||
selectedStartArea.value = area
|
||||
}
|
||||
|
||||
const handleCancelStartSelection = () => {
|
||||
syncDraftStartSelection()
|
||||
isStartPanelOpen.value = false
|
||||
}
|
||||
|
||||
const handleConfirmStartSelection = () => {
|
||||
confirmedStart.value = {
|
||||
label: pendingStartLabel.value,
|
||||
floor: selectedStartFloor.value,
|
||||
source: 'facility-detail'
|
||||
}
|
||||
isStartPanelOpen.value = false
|
||||
}
|
||||
|
||||
const handleStartNavigation = () => {
|
||||
const params: Record<string, string> = {
|
||||
facilityId: facility.value.id,
|
||||
target: facility.value.name,
|
||||
state: 'preview'
|
||||
}
|
||||
|
||||
if (confirmedStart.value) {
|
||||
params.startLabel = confirmedStart.value.label
|
||||
params.startFloor = confirmedStart.value.floor
|
||||
params.startSource = confirmedStart.value.source
|
||||
}
|
||||
|
||||
const query = Object.entries(params)
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
.join('&')
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?${query}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
activeMode.value = mode
|
||||
uni.showToast({
|
||||
title: mode === '2d' ? '已切换馆外入口参考' : '已切换馆内三维位置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
activeFloor.value = floor
|
||||
}
|
||||
|
||||
const handleToolClick = (tool: string) => {
|
||||
if (tool === '2D') {
|
||||
handleModeChange('2d')
|
||||
return
|
||||
}
|
||||
const handleCloseDetailPanel = () => {
|
||||
isDetailPanelVisible.value = false
|
||||
}
|
||||
|
||||
if (tool === '3D') {
|
||||
handleModeChange('3d')
|
||||
return
|
||||
}
|
||||
|
||||
if (tool === '重置') {
|
||||
activeMode.value = '3d'
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: tool === '重置' ? '已回到三维位置预览' : '该工具暂未开放',
|
||||
icon: 'none'
|
||||
})
|
||||
const handleOpenDetailPanel = () => {
|
||||
isDetailPanelVisible.value = true
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
@@ -335,86 +350,106 @@ const handlePageBack = () => {
|
||||
<style scoped lang="scss">
|
||||
.detail-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 84px;
|
||||
right: 16px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 116px);
|
||||
height: 252px;
|
||||
padding: 22px 20px 20px;
|
||||
min-height: 110px;
|
||||
padding: 14px 14px 12px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14);
|
||||
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: space-between;
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 18px;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
color: #151713;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.open-status {
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
color: #1a7f37;
|
||||
}
|
||||
|
||||
.detail-lines {
|
||||
margin-top: 17px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-line {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: #6b7178;
|
||||
.detail-subtitle {
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: #696962;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
margin-top: 21px;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
height: 28px;
|
||||
min-width: 88px;
|
||||
padding: 0 15px;
|
||||
height: 26px;
|
||||
min-width: 68px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #e5e6de;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-tag.active {
|
||||
background: #000000;
|
||||
border-color: #000000;
|
||||
background: #151713;
|
||||
border-color: #151713;
|
||||
}
|
||||
|
||||
.detail-tag-text {
|
||||
@@ -428,191 +463,58 @@ const handlePageBack = () => {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
width: 151px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
width: 168px;
|
||||
background: #000000;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.action-btn.primary .action-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.start-panel-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: 52;
|
||||
}
|
||||
|
||||
.start-select-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 116px);
|
||||
height: 276px;
|
||||
padding: 22px 20px 16px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16);
|
||||
z-index: 55;
|
||||
}
|
||||
|
||||
.start-select-title {
|
||||
.detail-hint {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.start-select-desc {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: #626970;
|
||||
}
|
||||
|
||||
.start-chip-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.start-chip-section.area {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.start-chip-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #60666d;
|
||||
color: #1a7f37;
|
||||
}
|
||||
|
||||
.start-chip-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.start-chip {
|
||||
height: 28px;
|
||||
min-width: 48px;
|
||||
padding: 0 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #f4f4ef;
|
||||
border: 1px solid #e7e8e2;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.start-chip.area {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.start-chip.active {
|
||||
background: #000000;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
.start-chip-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #60666d;
|
||||
}
|
||||
|
||||
.start-chip.active .start-chip-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.start-panel-actions {
|
||||
.detail-restore {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 16px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.start-panel-btn {
|
||||
height: 42px;
|
||||
right: 18px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 266px);
|
||||
height: 40px;
|
||||
min-width: 74px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid #dde5df;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.start-panel-btn.secondary {
|
||||
width: 151px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
.target-dot.small {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: 0;
|
||||
border-width: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.start-panel-btn.primary {
|
||||
width: 168px;
|
||||
background: #000000;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.start-panel-btn-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.start-panel-btn.primary .start-panel-btn-text {
|
||||
color: var(--museum-accent);
|
||||
.detail-restore-text {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 700;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.start-panel-actions {
|
||||
gap: 10px;
|
||||
.detail-sheet {
|
||||
left: 78px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.start-panel-btn.secondary,
|
||||
.start-panel-btn.primary {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
.detail-tag {
|
||||
min-width: 0;
|
||||
padding: 0 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user