chore: initialize frontend miniapp repository
This commit is contained in:
90
src/components/navigation/BottomTabBar.vue
Normal file
90
src/components/navigation/BottomTabBar.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="bottom-tab-bar">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === tab.id }"
|
||||
@tap="handleTabClick(tab.id)"
|
||||
>
|
||||
<view class="tab-icon">
|
||||
<text class="icon">{{ tab.icon }}</text>
|
||||
</view>
|
||||
<text class="tab-label">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Tab {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
current?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [tabId: string]
|
||||
}>()
|
||||
|
||||
const currentTab = ref(props.current || 'hall')
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: 'hall', label: '展厅', icon: '🏛️' },
|
||||
{ id: 'exhibit', label: '展品', icon: '🖼️' },
|
||||
{ id: 'facility', label: '设施', icon: '🚻' },
|
||||
{ id: 'route', label: '路线', icon: '🗺️' }
|
||||
]
|
||||
|
||||
const handleTabClick = (tabId: string) => {
|
||||
currentTab.value = tabId
|
||||
emit('change', tabId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bottom-tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
background-color: var(--museum-bg-surface);
|
||||
border-top: 1px solid var(--museum-border-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 12px;
|
||||
color: var(--museum-text-secondary);
|
||||
}
|
||||
|
||||
.tab-item.active .tab-label {
|
||||
color: var(--museum-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
514
src/components/navigation/GuideMapShell.vue
Normal file
514
src/components/navigation/GuideMapShell.vue
Normal file
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<view class="guide-map-shell">
|
||||
<view class="map-layer" :class="`map-${mapType}`">
|
||||
<template v-if="mapType === 'indoor'">
|
||||
<!-- #ifdef H5 -->
|
||||
<ThreeMap
|
||||
ref="threeMapRef"
|
||||
class="indoor-three-map"
|
||||
:asset-base-url="indoorAssetBaseUrl"
|
||||
:show-controls="false"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="indoor-platform-fallback">
|
||||
<text class="indoor-platform-title">室内三维导览</text>
|
||||
<text class="indoor-platform-desc">当前端暂不支持 WebGL 三维展示,请在 H5 端查看。</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
<view v-else class="outdoor-map-bg">
|
||||
<TencentMap class="outdoor-map" />
|
||||
</view>
|
||||
<view v-if="dimmed" class="dim-layer"></view>
|
||||
</view>
|
||||
|
||||
<view v-if="showSearch" class="guide-search-field" :style="searchFieldStyle" @tap="handleSearchTap">
|
||||
<text class="search-field-text">{{ searchText }}</text>
|
||||
<view class="search-icon">
|
||||
<view class="search-icon-circle"></view>
|
||||
<view class="search-icon-handle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="guide-mode-row"
|
||||
:class="`layout-${modeLayout}`"
|
||||
:style="modeRowStyle"
|
||||
>
|
||||
<template v-if="modeLayout === 'full'">
|
||||
<view
|
||||
class="mode-option"
|
||||
:class="{ active: activeMode === '2d' }"
|
||||
@tap="handleModeChange('2d')"
|
||||
>
|
||||
<text class="mode-label">室外2D</text>
|
||||
</view>
|
||||
<view
|
||||
class="mode-option"
|
||||
:class="{ active: activeMode === '3d' }"
|
||||
@tap="handleModeChange('3d')"
|
||||
>
|
||||
<text class="mode-label">室内3D</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="compact-mode-switch">
|
||||
<view
|
||||
class="mode-option"
|
||||
:class="{ active: activeMode === '2d' }"
|
||||
@tap="handleModeChange('2d')"
|
||||
>
|
||||
<text class="mode-label">室外2D</text>
|
||||
</view>
|
||||
<view
|
||||
class="mode-option"
|
||||
:class="{ active: activeMode === '3d' }"
|
||||
@tap="handleModeChange('3d')"
|
||||
>
|
||||
<text class="mode-label">室内3D</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mode-status-chip" :class="`tone-${modeStatusTone}`">
|
||||
<text class="mode-status-text">{{ modeStatus }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<slot name="overlay"></slot>
|
||||
|
||||
<view v-if="showFloor" class="floor-switcher" :style="floorSwitcherStyle">
|
||||
<view
|
||||
v-for="floor in floors"
|
||||
:key="floor"
|
||||
class="floor-item"
|
||||
:class="{ active: activeFloor === floor }"
|
||||
@tap="handleFloorChange(floor)"
|
||||
>
|
||||
<text class="floor-label">{{ floor }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="tools.length" class="tool-stack" :style="toolStackStyle">
|
||||
<view
|
||||
v-for="tool in tools"
|
||||
:key="tool"
|
||||
class="tool-btn"
|
||||
@tap="handleToolClick(tool)"
|
||||
>
|
||||
<text class="tool-label">{{ tool }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import TencentMap from '@/components/map/TencentMap.vue'
|
||||
// #ifdef H5
|
||||
import ThreeMap from '@/components/map/ThreeMap.vue'
|
||||
// #endif
|
||||
import {
|
||||
NAV_ASSET_BASE_URL,
|
||||
NAV_FLOOR_OPTIONS,
|
||||
navFloorIdFromLabel
|
||||
} from '@/services/navAssets'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
searchText?: string
|
||||
activeMode?: '2d' | '3d'
|
||||
activeFloor?: string
|
||||
searchTop?: string
|
||||
floorTop?: string
|
||||
toolsTop?: string
|
||||
tools?: string[]
|
||||
showSearch?: boolean
|
||||
showFloor?: boolean
|
||||
modeTop?: string
|
||||
modeLayout?: 'full' | 'status'
|
||||
modeStatus?: string
|
||||
modeStatusTone?: 'solid' | 'glass'
|
||||
mapType?: 'indoor' | 'outdoor'
|
||||
outdoorVariant?: 'home' | 'entrance'
|
||||
indoorAssetBaseUrl?: string
|
||||
dimmed?: boolean
|
||||
}>(), {
|
||||
searchText: '搜索设施、展厅、入口',
|
||||
activeMode: '3d',
|
||||
activeFloor: '1F',
|
||||
searchTop: '16px',
|
||||
floorTop: '164px',
|
||||
toolsTop: '406px',
|
||||
tools: () => [] as string[],
|
||||
showSearch: true,
|
||||
showFloor: true,
|
||||
modeTop: '60px',
|
||||
modeLayout: 'full',
|
||||
modeStatus: '',
|
||||
modeStatusTone: 'solid',
|
||||
mapType: 'indoor',
|
||||
outdoorVariant: 'home',
|
||||
indoorAssetBaseUrl: NAV_ASSET_BASE_URL,
|
||||
dimmed: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
searchTap: []
|
||||
modeChange: [mode: '2d' | '3d']
|
||||
floorChange: [floor: string]
|
||||
toolClick: [tool: string]
|
||||
}>()
|
||||
|
||||
const threeMapRef = ref<{ switchFloor?: (floorId: string) => Promise<void> | void } | null>(null)
|
||||
const floors = NAV_FLOOR_OPTIONS.map((floor) => floor.label)
|
||||
|
||||
const searchFieldStyle = computed(() => ({
|
||||
top: props.searchTop
|
||||
}))
|
||||
|
||||
const floorSwitcherStyle = computed(() => ({
|
||||
top: props.floorTop
|
||||
}))
|
||||
|
||||
const modeRowStyle = computed(() => ({
|
||||
top: props.modeTop
|
||||
}))
|
||||
|
||||
const toolStackStyle = computed(() => ({
|
||||
top: props.toolsTop
|
||||
}))
|
||||
|
||||
const handleSearchTap = () => {
|
||||
emit('searchTap')
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
emit('modeChange', mode)
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
const floorId = navFloorIdFromLabel(floor)
|
||||
void threeMapRef.value?.switchFloor?.(floorId)
|
||||
emit('floorChange', floor)
|
||||
}
|
||||
|
||||
const handleToolClick = (tool: string) => {
|
||||
emit('toolClick', tool)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.guide-map-shell {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.map-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
background: var(--museum-bg-map);
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.indoor-three-map,
|
||||
.outdoor-map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dim-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.outdoor-map-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: #f6f8f5;
|
||||
}
|
||||
|
||||
.indoor-platform-fallback {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
background: #f1f3ee;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.indoor-platform-title {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.indoor-platform-desc {
|
||||
max-width: 260px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #626a73;
|
||||
}
|
||||
|
||||
.guide-search-field {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
background: var(--museum-bg-cream);
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 6px;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.search-field-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
color: #8b8b84;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-icon-circle {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 1px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border: 1.4px solid #1f2329;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-icon-handle {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
width: 8px;
|
||||
height: 1.4px;
|
||||
background: #1f2329;
|
||||
border-radius: 2px;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.guide-mode-row {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
height: 34px;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.guide-mode-row.layout-full {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6e6dd;
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.guide-mode-row.layout-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.compact-mode-switch {
|
||||
width: 220px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(230, 230, 230, 0.5);
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.mode-option {
|
||||
flex: 1;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 13px;
|
||||
}
|
||||
|
||||
.layout-status .mode-option {
|
||||
width: 106px;
|
||||
flex: 0 0 106px;
|
||||
}
|
||||
|
||||
.mode-option.active {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.mode-option.active .mode-label {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.mode-status-chip {
|
||||
width: 111px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.mode-status-chip.tone-glass {
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mode-status-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #696962;
|
||||
}
|
||||
|
||||
.floor-switcher {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
width: 46px;
|
||||
height: 218px;
|
||||
padding: 1px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
border: 1px solid #dde5df;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.floor-item {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.floor-item:first-child {
|
||||
border-radius: 7px 7px 0 0;
|
||||
}
|
||||
|
||||
.floor-item:last-child {
|
||||
border-radius: 0 0 7px 7px;
|
||||
}
|
||||
|
||||
.floor-item:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
right: 7px;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background: #ecede7;
|
||||
}
|
||||
|
||||
.floor-item.active {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.floor-label {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
color: #545861;
|
||||
}
|
||||
|
||||
.floor-item.active .floor-label {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.tool-stack {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid #e4e5df;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.guide-map-shell {
|
||||
max-width: 430px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 12px 36px rgba(36, 49, 42, 0.16);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user