970 lines
22 KiB
Vue
970 lines
22 KiB
Vue
<template>
|
|
<view class="explain-list">
|
|
<view class="spatial-stage">
|
|
<view class="stage-copy">
|
|
<text class="stage-kicker">空间讲解</text>
|
|
<text class="stage-title">{{ activeFloorLabel }} · {{ filteredCards.length }} 个讲解点</text>
|
|
<text class="stage-desc">
|
|
{{ playableCount }} 个可播放音频 · {{ hallSummaries.length }} 个展厅
|
|
</text>
|
|
</view>
|
|
<view class="stage-map">
|
|
<view class="stage-floor">
|
|
<text class="stage-floor-text">{{ activeFloorLabel }}</text>
|
|
</view>
|
|
<view
|
|
v-for="(marker, index) in previewMarkers"
|
|
:key="marker.id"
|
|
class="stage-marker"
|
|
:class="`marker-${index + 1}`"
|
|
></view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="content-panel">
|
|
<view class="panel-handle"></view>
|
|
|
|
<view class="panel-summary">
|
|
<view class="summary-main">
|
|
<text class="summary-title">当前楼层 {{ activeFloorLabel }}</text>
|
|
<text class="summary-subtitle">
|
|
{{ filteredCards.length }} 个讲解点 · {{ unavailableAudioCount }} 个音频待开放
|
|
</text>
|
|
</view>
|
|
<view class="summary-chip">
|
|
<text class="summary-chip-text">位置预览</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="search-box">
|
|
<svg class="search-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" stroke-width="1.5"/>
|
|
<path d="M12.5 12.5L17 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
<input
|
|
class="search-input"
|
|
type="text"
|
|
placeholder="搜索讲解、展厅、主题"
|
|
v-model="searchInputValue"
|
|
@input="handleSearchInput"
|
|
@confirm="handleSearchConfirm"
|
|
/>
|
|
<view v-if="searchInputValue" class="clear-btn" @tap="handleClearSearch">
|
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
<circle cx="7" cy="7" r="6" fill="#8b9183"/>
|
|
<path d="M4.5 4.5L9.5 9.5M9.5 4.5L4.5 9.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
</view>
|
|
</view>
|
|
|
|
<scroll-view class="panel-scroll" :scroll-y="true" :show-scrollbar="false">
|
|
<view v-if="loading" class="state-block">
|
|
<text class="state-title">正在加载讲解内容</text>
|
|
<text class="state-desc">稍后将展示当前楼层和展厅讲解点。</text>
|
|
</view>
|
|
|
|
<view v-else-if="error" class="state-block error">
|
|
<text class="state-title">讲解内容加载失败</text>
|
|
<text class="state-desc">{{ error }}</text>
|
|
</view>
|
|
|
|
<template v-else>
|
|
<view v-if="featuredCard" class="featured-section">
|
|
<view class="section-heading">
|
|
<text class="section-title">今日推荐讲解</text>
|
|
<text class="section-count">{{ featuredCard.audioStatusText }}</text>
|
|
</view>
|
|
<view class="featured-card" @tap="handleCardClick(featuredCard)">
|
|
<image
|
|
v-if="featuredCard.coverImage"
|
|
class="featured-cover"
|
|
:src="featuredCard.coverImage"
|
|
mode="aspectFill"
|
|
/>
|
|
<view v-else class="featured-cover placeholder">
|
|
<text class="placeholder-text">{{ contentTypeText(featuredCard.contentType).slice(0, 1) }}</text>
|
|
</view>
|
|
<view class="featured-body">
|
|
<text class="featured-title">{{ featuredCard.title }}</text>
|
|
<text class="featured-meta">{{ cardMeta(featuredCard) }}</text>
|
|
<text class="featured-summary">{{ featuredCard.summary || '正式讲解文案待内容库接入后完善。' }}</text>
|
|
<view class="featured-actions">
|
|
<view
|
|
v-if="featuredCard.audioStatus === 'playable'"
|
|
class="action-btn primary"
|
|
@tap.stop="handleAudioClick(featuredCard)"
|
|
>
|
|
<text class="action-text primary">开始讲解</text>
|
|
</view>
|
|
<view v-else class="action-btn disabled">
|
|
<text class="action-text disabled">{{ featuredCard.audioStatusText }}</text>
|
|
</view>
|
|
<view
|
|
v-if="featuredCard.poiId"
|
|
class="action-btn secondary"
|
|
@tap.stop="handleLocationClick(featuredCard)"
|
|
>
|
|
<text class="action-text">查看位置</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="filter-section">
|
|
<view
|
|
v-for="filter in resolvedFilters"
|
|
:key="filter.id"
|
|
class="filter-chip"
|
|
:class="{ active: activeFilter === filter.id }"
|
|
@tap="handleFilterChange(filter.id)"
|
|
>
|
|
<text class="filter-text">{{ filter.label }}</text>
|
|
<text v-if="typeof filter.count === 'number'" class="filter-count">{{ filter.count }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="hallSummaries.length" class="hall-section">
|
|
<view class="section-heading">
|
|
<text class="section-title">展厅讲解</text>
|
|
<text class="section-count">{{ hallSummaries.length }} 个展厅</text>
|
|
</view>
|
|
<view class="hall-grid">
|
|
<view
|
|
v-for="hall in hallSummaries"
|
|
:key="hall.name"
|
|
class="hall-chip"
|
|
@tap="handleHallFilter(hall.name)"
|
|
>
|
|
<text class="hall-name">{{ hall.name }}</text>
|
|
<text class="hall-count">{{ hall.count }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="list-section">
|
|
<view class="section-heading">
|
|
<text class="section-title">讲解列表</text>
|
|
<text class="section-count">{{ displayedCards.length }} 条</text>
|
|
</view>
|
|
|
|
<view v-if="displayedCards.length" class="card-list">
|
|
<view
|
|
v-for="card in displayedCards"
|
|
:key="card.id"
|
|
class="explain-card"
|
|
@tap="handleCardClick(card)"
|
|
>
|
|
<view class="card-main">
|
|
<view class="card-title-row">
|
|
<text class="card-title">{{ card.title }}</text>
|
|
<view class="status-pill" :class="`status-${card.audioStatus}`">
|
|
<text class="status-text">{{ card.audioStatusText }}</text>
|
|
</view>
|
|
</view>
|
|
<text class="card-meta">{{ cardMeta(card) }}</text>
|
|
<text v-if="card.summary" class="card-summary">{{ card.summary }}</text>
|
|
<view class="badge-row">
|
|
<text
|
|
v-for="badge in normalizedBadges(card)"
|
|
:key="`${card.id}-${badge}`"
|
|
class="badge"
|
|
>
|
|
{{ badge }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<view class="card-actions">
|
|
<view
|
|
v-if="card.audioStatus === 'playable'"
|
|
class="small-action primary"
|
|
@tap.stop="handleAudioClick(card)"
|
|
>
|
|
<text class="small-action-text primary">播放</text>
|
|
</view>
|
|
<view class="small-action" @tap.stop="handleCardClick(card)">
|
|
<text class="small-action-text">详情</text>
|
|
</view>
|
|
<view
|
|
v-if="card.poiId"
|
|
class="small-action"
|
|
@tap.stop="handleLocationClick(card)"
|
|
>
|
|
<text class="small-action-text">位置</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="state-block empty">
|
|
<text class="state-title">未找到相关讲解</text>
|
|
<text class="state-desc">试试展厅名称、楼层或主题关键词。</text>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
</scroll-view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import type {
|
|
ExplainCardViewModel
|
|
} from '@/view-models/explainViewModels'
|
|
|
|
export interface ExplainFilterOption {
|
|
id: 'all' | 'hall' | 'theme' | 'nearby' | 'playable'
|
|
label: string
|
|
count?: number
|
|
}
|
|
|
|
export type Exhibit = ExplainCardViewModel
|
|
|
|
const props = withDefaults(defineProps<{
|
|
cards?: ExplainCardViewModel[]
|
|
exhibits?: ExplainCardViewModel[]
|
|
filters?: ExplainFilterOption[]
|
|
loading?: boolean
|
|
error?: string
|
|
hotSearches?: string[]
|
|
}>(), {
|
|
cards: () => [],
|
|
exhibits: () => [],
|
|
filters: () => [],
|
|
loading: false,
|
|
error: '',
|
|
hotSearches: () => []
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
exhibitClick: [card: ExplainCardViewModel]
|
|
featuredClick: [card: ExplainCardViewModel]
|
|
audioClick: [card: ExplainCardViewModel]
|
|
locationClick: [card: ExplainCardViewModel]
|
|
filterChange: [filter: ExplainFilterOption['id']]
|
|
search: [keyword: string]
|
|
}>()
|
|
|
|
const searchInputValue = ref('')
|
|
const activeFilter = ref<ExplainFilterOption['id']>('all')
|
|
const activeHallName = ref('')
|
|
|
|
const cards = computed(() => props.cards.length ? props.cards : props.exhibits)
|
|
|
|
const playableCount = computed(() => cards.value.filter((card) => card.audioStatus === 'playable').length)
|
|
const unavailableAudioCount = computed(() => cards.value.filter((card) => card.audioStatus === 'unavailable').length)
|
|
|
|
const hallSummaries = computed(() => {
|
|
const result = new Map<string, number>()
|
|
cards.value.forEach((card) => {
|
|
const hallName = card.hallName || '未分配展厅'
|
|
result.set(hallName, (result.get(hallName) || 0) + 1)
|
|
})
|
|
return Array.from(result.entries())
|
|
.map(([name, count]) => ({ name, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
})
|
|
|
|
const activeFloorLabel = computed(() => {
|
|
const counts = new Map<string, number>()
|
|
cards.value.forEach((card) => {
|
|
if (card.floorLabel) {
|
|
counts.set(card.floorLabel, (counts.get(card.floorLabel) || 0) + 1)
|
|
}
|
|
})
|
|
|
|
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || '全馆'
|
|
})
|
|
|
|
const previewMarkers = computed(() => cards.value.slice(0, 5))
|
|
|
|
const resolvedFilters = computed<ExplainFilterOption[]>(() => props.filters.length ? props.filters : [
|
|
{ id: 'all', label: '全部', count: cards.value.length },
|
|
{ id: 'hall', label: '展厅', count: hallSummaries.value.length },
|
|
{ id: 'theme', label: '主题', count: props.hotSearches.length || hallSummaries.value.length },
|
|
{ id: 'nearby', label: '附近', count: cards.value.filter((card) => card.floorLabel === activeFloorLabel.value).length },
|
|
{ id: 'playable', label: '可播放', count: playableCount.value }
|
|
])
|
|
|
|
const keywordMatchedCards = computed(() => {
|
|
const keyword = searchInputValue.value.trim().toLowerCase()
|
|
if (!keyword) return cards.value
|
|
|
|
return cards.value.filter((card) => [
|
|
card.title,
|
|
card.subtitle,
|
|
card.summary,
|
|
card.hallName,
|
|
card.floorLabel,
|
|
card.audioStatusText,
|
|
contentTypeText(card.contentType),
|
|
...card.badges
|
|
].filter(Boolean).join(' ').toLowerCase().includes(keyword))
|
|
})
|
|
|
|
const filteredCards = computed(() => {
|
|
let result = keywordMatchedCards.value
|
|
|
|
if (activeHallName.value) {
|
|
result = result.filter((card) => card.hallName === activeHallName.value)
|
|
}
|
|
|
|
if (activeFilter.value === 'nearby') {
|
|
result = result.filter((card) => card.floorLabel === activeFloorLabel.value)
|
|
}
|
|
|
|
if (activeFilter.value === 'playable') {
|
|
result = result.filter((card) => card.audioStatus === 'playable')
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
const displayedCards = computed(() => filteredCards.value)
|
|
const featuredCard = computed(() => cards.value.find((card) => card.audioStatus === 'playable') || cards.value[0])
|
|
|
|
const contentTypeText = (type: ExplainCardViewModel['contentType']) => {
|
|
const textMap: Record<ExplainCardViewModel['contentType'], string> = {
|
|
hall: '展厅讲解',
|
|
zone: '展区讲解',
|
|
exhibit: '讲解点',
|
|
theme: '主题讲解'
|
|
}
|
|
return textMap[type]
|
|
}
|
|
|
|
const cardMeta = (card: ExplainCardViewModel) => [
|
|
card.hallName,
|
|
card.floorLabel,
|
|
card.durationLabel,
|
|
card.languageLabel,
|
|
contentTypeText(card.contentType)
|
|
].filter(Boolean).join(' · ')
|
|
|
|
const normalizedBadges = (card: ExplainCardViewModel) => {
|
|
const badges = card.badges.length ? card.badges : [contentTypeText(card.contentType)]
|
|
return badges.slice(0, 3)
|
|
}
|
|
|
|
const handleSearchInput = () => {
|
|
emit('search', searchInputValue.value)
|
|
}
|
|
|
|
const handleSearchConfirm = () => {
|
|
emit('search', searchInputValue.value.trim())
|
|
}
|
|
|
|
const handleClearSearch = () => {
|
|
searchInputValue.value = ''
|
|
activeHallName.value = ''
|
|
emit('search', '')
|
|
}
|
|
|
|
const handleFilterChange = (filter: ExplainFilterOption['id']) => {
|
|
activeFilter.value = filter
|
|
if (filter !== 'hall') {
|
|
activeHallName.value = ''
|
|
}
|
|
emit('filterChange', filter)
|
|
}
|
|
|
|
const handleHallFilter = (hallName: string) => {
|
|
activeFilter.value = 'hall'
|
|
activeHallName.value = activeHallName.value === hallName ? '' : hallName
|
|
emit('filterChange', 'hall')
|
|
}
|
|
|
|
const handleCardClick = (card: ExplainCardViewModel) => {
|
|
emit('exhibitClick', card)
|
|
}
|
|
|
|
const handleAudioClick = (card: ExplainCardViewModel) => {
|
|
emit('audioClick', card)
|
|
}
|
|
|
|
const handleLocationClick = (card: ExplainCardViewModel) => {
|
|
emit('locationClick', card)
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.explain-list {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: #edf0ea;
|
|
}
|
|
|
|
.spatial-stage {
|
|
position: absolute;
|
|
top: 44px;
|
|
left: 0;
|
|
right: 0;
|
|
height: 298px;
|
|
padding: 18px 16px 0;
|
|
box-sizing: border-box;
|
|
background: #dfe5dc;
|
|
}
|
|
|
|
.stage-copy {
|
|
position: relative;
|
|
z-index: 2;
|
|
width: 220px;
|
|
}
|
|
|
|
.stage-kicker,
|
|
.stage-title,
|
|
.stage-desc {
|
|
display: block;
|
|
}
|
|
|
|
.stage-kicker {
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
color: #5e6658;
|
|
}
|
|
|
|
.stage-title {
|
|
margin-top: 4px;
|
|
font-size: 22px;
|
|
line-height: 30px;
|
|
font-weight: 700;
|
|
color: #151713;
|
|
}
|
|
|
|
.stage-desc {
|
|
margin-top: 6px;
|
|
font-size: 13px;
|
|
line-height: 18px;
|
|
color: #5e6658;
|
|
}
|
|
|
|
.stage-map {
|
|
position: absolute;
|
|
left: 26px;
|
|
right: 26px;
|
|
bottom: 38px;
|
|
height: 128px;
|
|
border: 1px solid rgba(88, 95, 78, 0.2);
|
|
border-radius: 8px;
|
|
background: #f4f5ef;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stage-map::before,
|
|
.stage-map::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 32px;
|
|
right: 32px;
|
|
height: 1px;
|
|
background: rgba(88, 95, 78, 0.2);
|
|
}
|
|
|
|
.stage-map::before {
|
|
top: 42px;
|
|
}
|
|
|
|
.stage-map::after {
|
|
top: 84px;
|
|
}
|
|
|
|
.stage-floor {
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 12px;
|
|
min-width: 44px;
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
background: #151713;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.stage-floor-text {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--museum-accent);
|
|
}
|
|
|
|
.stage-marker {
|
|
position: absolute;
|
|
width: 14px;
|
|
height: 14px;
|
|
background: #2f7d59;
|
|
border: 3px solid #ffffff;
|
|
border-radius: 50%;
|
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.marker-1 {
|
|
left: 28%;
|
|
top: 36%;
|
|
}
|
|
|
|
.marker-2 {
|
|
left: 48%;
|
|
top: 54%;
|
|
}
|
|
|
|
.marker-3 {
|
|
left: 62%;
|
|
top: 30%;
|
|
}
|
|
|
|
.marker-4 {
|
|
left: 74%;
|
|
top: 62%;
|
|
}
|
|
|
|
.marker-5 {
|
|
left: 38%;
|
|
top: 68%;
|
|
}
|
|
|
|
.content-panel {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: 232px;
|
|
bottom: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-sizing: border-box;
|
|
background: #ffffff;
|
|
border-radius: 16px 16px 0 0;
|
|
box-shadow: 0 -8px 28px rgba(25, 28, 21, 0.08);
|
|
z-index: 10;
|
|
}
|
|
|
|
.panel-handle {
|
|
align-self: center;
|
|
width: 64px;
|
|
height: 4px;
|
|
margin-top: 8px;
|
|
border-radius: 2px;
|
|
background: #d9ddd3;
|
|
}
|
|
|
|
.panel-summary {
|
|
flex-shrink: 0;
|
|
padding: 12px 16px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.summary-main {
|
|
min-width: 0;
|
|
}
|
|
|
|
.summary-title,
|
|
.summary-subtitle {
|
|
display: block;
|
|
}
|
|
|
|
.summary-title {
|
|
font-size: 18px;
|
|
line-height: 25px;
|
|
font-weight: 700;
|
|
color: #151713;
|
|
}
|
|
|
|
.summary-subtitle {
|
|
margin-top: 2px;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
color: #697064;
|
|
}
|
|
|
|
.summary-chip {
|
|
flex-shrink: 0;
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #eef1e8;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.summary-chip-text {
|
|
font-size: 12px;
|
|
color: #495143;
|
|
}
|
|
|
|
.search-box {
|
|
flex-shrink: 0;
|
|
margin: 0 16px 12px;
|
|
height: 42px;
|
|
display: flex;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
background: #f5f6f1;
|
|
border: 1px solid #e2e5dc;
|
|
border-radius: 8px;
|
|
padding: 0 12px;
|
|
}
|
|
|
|
.search-icon {
|
|
flex-shrink: 0;
|
|
color: #596052;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
height: 40px;
|
|
font-size: 14px;
|
|
color: #151713;
|
|
background: transparent;
|
|
}
|
|
|
|
.clear-btn {
|
|
flex-shrink: 0;
|
|
width: 22px;
|
|
height: 22px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.panel-scroll {
|
|
flex: 1;
|
|
height: 0;
|
|
padding: 0 16px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.section-heading {
|
|
margin: 12px 0 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 15px;
|
|
line-height: 21px;
|
|
font-weight: 700;
|
|
color: #151713;
|
|
}
|
|
|
|
.section-count {
|
|
font-size: 12px;
|
|
color: #788072;
|
|
}
|
|
|
|
.featured-card {
|
|
display: flex;
|
|
padding: 10px;
|
|
box-sizing: border-box;
|
|
background: #f7f8f3;
|
|
border: 1px solid #ebece5;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.featured-cover {
|
|
flex-shrink: 0;
|
|
width: 82px;
|
|
height: 94px;
|
|
border-radius: 8px;
|
|
background: #e5e8df;
|
|
}
|
|
|
|
.featured-cover.placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.placeholder-text {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #68725d;
|
|
}
|
|
|
|
.featured-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin-left: 12px;
|
|
}
|
|
|
|
.featured-title,
|
|
.featured-meta,
|
|
.featured-summary {
|
|
display: block;
|
|
}
|
|
|
|
.featured-title {
|
|
font-size: 17px;
|
|
line-height: 24px;
|
|
font-weight: 700;
|
|
color: #151713;
|
|
}
|
|
|
|
.featured-meta,
|
|
.card-meta {
|
|
margin-top: 3px;
|
|
font-size: 12px;
|
|
line-height: 17px;
|
|
color: #68725d;
|
|
}
|
|
|
|
.featured-summary,
|
|
.card-summary {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
color: #4f574b;
|
|
display: -webkit-box;
|
|
overflow: hidden;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
}
|
|
|
|
.featured-actions,
|
|
.card-actions,
|
|
.filter-section,
|
|
.badge-row,
|
|
.hall-grid {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.featured-actions {
|
|
margin-top: 10px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-btn,
|
|
.small-action {
|
|
height: 32px;
|
|
padding: 0 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
border-radius: 8px;
|
|
background: #ffffff;
|
|
border: 1px solid #d8dbd1;
|
|
}
|
|
|
|
.action-btn.primary,
|
|
.small-action.primary {
|
|
background: #151713;
|
|
border-color: #151713;
|
|
}
|
|
|
|
.action-btn.disabled {
|
|
background: #eef1e8;
|
|
border-color: #eef1e8;
|
|
}
|
|
|
|
.action-text,
|
|
.small-action-text {
|
|
font-size: 13px;
|
|
color: #151713;
|
|
}
|
|
|
|
.action-text.primary,
|
|
.small-action-text.primary {
|
|
color: var(--museum-accent);
|
|
}
|
|
|
|
.action-text.disabled {
|
|
color: #68725d;
|
|
}
|
|
|
|
.filter-section {
|
|
margin-top: 14px;
|
|
gap: 8px;
|
|
overflow-x: auto;
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
.filter-chip {
|
|
flex-shrink: 0;
|
|
min-width: 58px;
|
|
height: 34px;
|
|
padding: 0 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
box-sizing: border-box;
|
|
border-radius: 8px;
|
|
background: #f1f3ed;
|
|
}
|
|
|
|
.filter-chip.active {
|
|
background: #151713;
|
|
}
|
|
|
|
.filter-text,
|
|
.filter-count {
|
|
font-size: 13px;
|
|
color: #4f574b;
|
|
}
|
|
|
|
.filter-count {
|
|
color: #8b9385;
|
|
}
|
|
|
|
.filter-chip.active .filter-text,
|
|
.filter-chip.active .filter-count {
|
|
color: var(--museum-accent);
|
|
}
|
|
|
|
.hall-grid {
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.hall-chip {
|
|
width: calc(50% - 4px);
|
|
height: 42px;
|
|
padding: 0 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-sizing: border-box;
|
|
background: #f7f8f3;
|
|
border: 1px solid #ebece5;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.hall-name {
|
|
font-size: 13px;
|
|
color: #151713;
|
|
}
|
|
|
|
.hall-count {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #68725d;
|
|
}
|
|
|
|
.card-list {
|
|
padding-bottom: calc(28px + env(safe-area-inset-bottom));
|
|
}
|
|
|
|
.explain-card {
|
|
padding: 12px;
|
|
margin-bottom: 10px;
|
|
box-sizing: border-box;
|
|
background: #f7f8f3;
|
|
border: 1px solid #ebece5;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.card-title-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.card-title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 16px;
|
|
line-height: 22px;
|
|
font-weight: 700;
|
|
color: #151713;
|
|
}
|
|
|
|
.status-pill {
|
|
flex-shrink: 0;
|
|
height: 24px;
|
|
padding: 0 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
border-radius: 6px;
|
|
background: #eef1e8;
|
|
}
|
|
|
|
.status-playable {
|
|
background: #e5e600;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 11px;
|
|
color: #4f574b;
|
|
}
|
|
|
|
.status-playable .status-text {
|
|
color: #151713;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge-row {
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.badge {
|
|
height: 22px;
|
|
padding: 0 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
background: #ecefe7;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
color: #596052;
|
|
}
|
|
|
|
.card-actions {
|
|
margin-top: 10px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.small-action {
|
|
height: 30px;
|
|
}
|
|
|
|
.state-block {
|
|
margin: 30px 0;
|
|
padding: 22px 16px;
|
|
box-sizing: border-box;
|
|
text-align: center;
|
|
background: #f7f8f3;
|
|
border: 1px solid #ebece5;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.state-title,
|
|
.state-desc {
|
|
display: block;
|
|
}
|
|
|
|
.state-title {
|
|
font-size: 15px;
|
|
line-height: 21px;
|
|
font-weight: 700;
|
|
color: #151713;
|
|
}
|
|
|
|
.state-desc {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
color: #68725d;
|
|
}
|
|
|
|
.state-block.error {
|
|
border-color: #e5c2c2;
|
|
background: #fff7f7;
|
|
}
|
|
</style>
|