Commit 287dcb71 authored by 罗超's avatar 罗超

优化页面,新增搜索组件

parent 2b6adfbb
......@@ -25,10 +25,21 @@ body {
-moz-osx-font-smoothing: grayscale;
}
.arco-dropdown-list-wrapper{
max-height: 50vh !important;
max-height: unset !important;
overflow: unset !important;
}
.arco-dropdown{
padding: 0 !important;
background-color: transparent !important;
box-shadow: unset !important;
border: none !important;
}
.arco-dropdown .arco-dropdown-list > div{
background: #fff;
border: 1px solid var(--color-fill-3);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
.overlap::before {
display: block;
......
......@@ -5,6 +5,7 @@
<icon-down class="ml-[4px] text-[12px] text-primary-500" />
</a-button>
<template #content>
<div>
<a-doption
v-for="option in LOCALE_OPTIONS"
:key="option.value"
......@@ -16,6 +17,7 @@
<span class="font-medium">{{ option.label }}</span>
</div>
</a-doption>
</div>
</template>
</a-dropdown>
</template>
......
......@@ -7,6 +7,7 @@
}"
ref="wrapperRef"
:style="wrapperStyle"
>
<!-- 外部左箭头 -->
<template v-if="isOutsideNav && props.navigation.enabled && showPrevArrow">
......@@ -637,6 +638,10 @@ const wrapperStyle = computed(() => {
style.marginLeft = `${horizontal}px`
style.marginRight = `${horizontal}px`
}
if(props.searchBox?.enabled){
style.zIndex = '10'
}
// 有 maxWidth 限制时,保持 auto 居中,不应用 horizontal 边距
return style
......
......@@ -124,6 +124,10 @@ import type { SearchBoxProps } from '@/types/searchBox'
import { useAdaptiveStyle } from '@/composables/useAdaptiveStyle'
import { useDataSource } from '@/composables/useDataSource'
import type { DataSourceOption } from '@/composables/useDataSource'
import HotKeywordService from '@/services/HotKeywordService'
import PlaceService from '@/services/PlaceService'
import { useSystemConfigStore, useUserStore } from '@/stores'
import router from '@/router'
const adaptive = useAdaptiveStyle()
......@@ -135,10 +139,14 @@ const searchValue = ref('')
// 下拉面板显示状态
const showDropdown = ref(false)
const systemConfigStore = useSystemConfigStore()
// 数据源
const { options, loadDataSource } = useDataSource()
const hotSearchesData = ref<DataSourceOption[]>([])
const hotDestinationsData = ref<DataSourceOption[]>([])
const hotSearchesDataMode = computed(() => props.hotSearches.dataMode || 'custom')
const hotDestinationsDataMode = computed(() => props.hotDestinations.dataMode || 'custom')
// ==================== 计算属性 ====================
......@@ -313,9 +321,21 @@ const shouldShowDropdown = computed(() => {
// 加载数据
const loadData = async () => {
// 加载热门搜索
if (props.hotSearches.enabled) {
if (props.hotSearches.useDataSource && props.hotSearches.dataSourceKey) {
if (hotSearchesDataMode.value === 'global') {
try {
const resp = await HotKeywordService.getListAsync()
const list = (resp as any).data ?? resp ?? []
hotSearchesData.value = (list || []).map((item: any) => ({
label: item.keyword,
value: item.id || item.keyword
}))
} catch (error) {
hotSearchesData.value = []
}
} else if (props.hotSearches.useDataSource && props.hotSearches.dataSourceKey) {
// 使用数据源
await loadDataSource(props.hotSearches.dataSourceKey)
hotSearchesData.value = options.value[props.hotSearches.dataSourceKey] || []
......@@ -326,24 +346,40 @@ const loadData = async () => {
value: tag
}))
}
} else {
hotSearchesData.value = []
}
// 加载热门目的地 - 固定使用 hot_destinations 数据源
// 加载热门目的地
if (props.hotDestinations.enabled) {
await loadDataSource('hot_destinations')
const allDestinations = options.value['hot_destinations'] || []
// 只显示手动选中的目的地,并按照 selectedIds 的顺序排列
const selectedIds = props.hotDestinations.selectedDestinations || []
if (selectedIds.length > 0) {
// 按照 selectedIds 的顺序映射数据
hotDestinationsData.value = selectedIds
.map(id => allDestinations.find(dest => dest.value === id))
.filter(dest => dest !== undefined) as DataSourceOption[]
if (hotDestinationsDataMode.value === 'global') {
try {
const resp = await PlaceService.GetGlobalPlaceAsync(systemConfigStore.distributorId||0)
const group = (resp as any).data ?? resp
const reList = group?.reList || []
hotDestinationsData.value = reList.map((item: any) => ({
label: item.placeDetail?.name || item.placeId,
value: item.placeId,
image: item.placeDetail?.backgroundImage
}))
} catch (error) {
}
} else {
hotDestinationsData.value = []
await loadDataSource('hot_destinations')
const allDestinations = options.value['hot_destinations'] || []
// 只显示手动选中的目的地,并按照 selectedIds 的顺序排列
const selectedIds = props.hotDestinations.selectedDestinations || []
if (selectedIds.length > 0) {
hotDestinationsData.value = selectedIds
.map(id => allDestinations.find(dest => dest.value === id))
.filter(dest => dest !== undefined) as DataSourceOption[]
} else {
hotDestinationsData.value = []
}
}
} else {
hotDestinationsData.value = []
}
}
......@@ -356,12 +392,13 @@ const selectTag = (tag: DataSourceOption) => {
// 选择目的地
const selectDestination = (dest: DataSourceOption) => {
searchValue.value = dest.label
console.log(dest)
// 如果有链接,可以跳转
if (dest.link) {
window.location.href = dest.link
} else {
handleSearch()
//handleSearch()
router.push(`/place/${dest.value}`)
}
}
......
......@@ -5,6 +5,11 @@ import pageTitleEn from './page/page-title-en.json'
export default {
// 国家名称(基于ISO2代码)
countries: enCountries,
search: {
placeholder: 'Search destinations, products or activities',
hotSearch: 'Hot Searches',
hotDestination: 'Popular Destinations',
},
login: {
// Page titles
title: 'Tten Joy',
......
......@@ -15,6 +15,7 @@
"commonPassengerInfo": "Common Passenger Info",
"distributionCenter": "Distribution Center",
"resetPassword": "Reset Password",
"editEmail": "Bind/Edit Email"
"editEmail": "Bind/Edit Email",
"placeDetail": "Place Detail"
}
}
\ No newline at end of file
......@@ -15,6 +15,7 @@
"commonPassengerInfo": "Thông tin hành khách thường dùng",
"distributionCenter": "Trung tâm phân phối",
"resetPassword": "Đặt lại mật khẩu",
"editEmail": "Liên kết/Sửa đổi email"
"editEmail": "Liên kết/Sửa đổi email",
"placeDetail": "Chi tiết địa điểm"
}
}
\ No newline at end of file
......@@ -19,6 +19,7 @@
"basicInfor": "基础资料",
"account": "账户信息",
"passengerList": "常用旅客",
"mailingAddressList": "邮寄地址"
"mailingAddressList": "邮寄地址",
"placeDetail": "目的地详情"
}
}
\ No newline at end of file
......@@ -15,6 +15,7 @@
"commonPassengerInfo": "常用旅客資訊",
"distributionCenter": "分銷中心",
"resetPassword": "重設密碼",
"editEmail": "綁定/修改郵箱"
"editEmail": "綁定/修改郵箱",
"placeDetail": "目的地詳情"
}
}
\ No newline at end of file
......@@ -5,6 +5,11 @@ import pageTitleVi from './page/page-title-vi.json'
export default {
// 国家名称(基于ISO2代码)
countries: viCountries,
search: {
placeholder: 'Tìm kiếm điểm đến, sản phẩm hoặc hoạt động',
hotSearch: 'Tìm kiếm nổi bật',
hotDestination: 'Điểm đến nổi bật',
},
login: {
// Tiêu đề trang
title: 'Tten Joy',
......
......@@ -5,6 +5,11 @@ import pageTitleZhCN from './page/page-title-zh-CN.json'
export default {
// 国家名称(基于ISO2代码)
countries: zhCNCountries,
search: {
placeholder: '搜索目的地、产品或活动',
hotSearch: '热门搜索',
hotDestination: '热门目的地',
},
login: {
// 页面标题
title: 'Tten Joy',
......
......@@ -5,6 +5,11 @@ import pageTitleZhTW from './page/page-title-zh-TW.json'
export default {
// 国家名称(基于ISO2代码)
countries: zhTWCountries,
search: {
placeholder: '搜尋目的地、產品或活動',
hotSearch: '熱門搜尋',
hotDestination: '熱門目的地',
},
login: {
// 頁面標題
title: 'Tten Joy',
......
......@@ -25,10 +25,10 @@
{{ t('header.hotDestinations') }}
</div>
<div class="pt-4 mb-2" v-for="(val,index) in hotDestinations" :key="index">
<div class="pt-4 mb-2" v-for="(val,index) in hotDestinations" :key="index">
<div class="font-bold text-md mb-3">{{ val.name }}</div>
<div class="grid grid-cols-4 gap-4">
<div class="rounded-md flex items-center justify-center cursor-pointer overlap relative overflow-hidden !aspect-[3/2] !object-cover" v-for="(item,index) in val.reList" :key="index">
<div @click="handleHotDestinationClick(item.placeId)" class="rounded-md flex items-center justify-center cursor-pointer overlap relative overflow-hidden !aspect-[3/2] !object-cover" v-for="(item,index) in val.reList" :key="index">
<a-image :src="item.placeDetail?.backgroundImage || ''" show-loader></a-image>
<div class="absolute bottom-0 left-0 right-0 p-2">
<div class="text-white text-sm font-bold">
......@@ -53,6 +53,7 @@
<span
v-for="place in country.children"
:key="place.value"
@click="handleHotDestinationClick(place.value)"
class="place-item destination-place-item rounded-md"
>
{{ place.label }}
......@@ -72,19 +73,25 @@
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import PlaceService, { type PlaceGroupOutputDto } from '@/services/PlaceService'
import PlaceService, { type PlaceGroupOutputDto, } from '@/services/PlaceService'
import type { PlaceTreeNode } from '@/types/place'
import { Message } from '@arco-design/web-vue'
import { useSystemConfigStore } from '@/stores'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const placeTree = ref<PlaceTreeNode[]>([])
const activeIndex = ref(-1)
const router = useRouter()
const activeContinent = computed(() => placeTree.value[activeIndex.value])
const systemConfigStore = useSystemConfigStore()
const hotDestinations = ref<PlaceGroupOutputDto[]>([])
const handleHotDestinationClick = (val: string) => {
router.push(`/place/${val}`)
}
const loadPlaces = async () => {
try {
const data = await PlaceService.getPlaceTreeAsync()
......
......@@ -2,12 +2,13 @@
<div class="w-[1200px] mx-2 py-2 flex items-center justify-between h-[60px] text-xs text-gray-700">
<div class="flex items-center gap-4">
<img
v-if="logo"
:src="logo"
alt="logo"
class="h-[30px] cursor-pointer"
@click="handleGoHome"
v-if="logo"
:src="logo"
alt="logo"
class="h-[40px] cursor-pointer mr-3"
@click="handleGoHome"
/>
<Search v-if="shouldShowSearch" />
</div>
<div class="flex items-center gap-[8px]">
<LanguageSwitcher />
......@@ -73,15 +74,16 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import CartDropdown, { type CartItem } from './CartDropdown.vue'
import { useRouter, useRoute } from 'vue-router'
import CartDropdown from './CartDropdown.vue'
import { useI18n } from 'vue-i18n'
import { useSystemConfigStore } from '@/stores/index'
import LanguageSwitcher from '@/components/common/LanguageSwitcher.vue'
import Search from './Search.vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const { t } = useI18n()
const systemConfigStore = useSystemConfigStore()
......@@ -108,38 +110,56 @@ const currencyOptions = [
type CurrencyCode = typeof currencyOptions[number]
const mockCartItems: CartItem[] = [
{
id: '1',
title: '【KKday獨家】香港 Chikawa Ramen Buta(吉伊卡哇)拉麵|預約證含飲品券',
subtitle: 'Chikawa Ramen Buta 預約證含飲品券',
currency: 'TWD',
price: 153,
quantity: 1,
date: '2025/12/20 12:30',
},
{
id: '2',
title: '永東巴士|香港來往澳門、大灣區各線路車票現金兌換券|全新線路',
subtitle: '永東巴士線路 HK$50 現金兌換券|適用於永東高線單程票',
currency: 'TWD',
price: 226,
quantity: 1,
date: '2025/12/19',
const selectedCurrencyCode = ref<CurrencyCode>('TWD')
const isLoggedIn = computed(() => Boolean(userStore.userInfo))
const userPhoto = computed(() => userStore.memberData?.photo || '')
const userInitial = computed(() => userStore.userInfo?.nick?.[0] || '')
// 首页滚动控制搜索框显示
const scrollY = ref(0)
const isHome = computed(() => route.path === '/' || route.path === '/home')
const shouldShowSearch = computed(() => {
if (!isHome.value) return true
return scrollY.value > 200
})
const handleScroll = () => {
scrollY.value = window.scrollY || document.documentElement.scrollTop || 0
}
const setupScrollListener = () => {
if (typeof window === 'undefined') return
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
}
const removeScrollListener = () => {
if (typeof window === 'undefined') return
window.removeEventListener('scroll', handleScroll)
}
watch(
() => route.path,
() => {
if (isHome.value) {
setupScrollListener()
} else {
removeScrollListener()
scrollY.value = 0
}
},
]
{ immediate: true }
)
const cartItems = computed<CartItem[]>(() => {
if (!userStore.userInfo) {
return []
onMounted(() => {
if (isHome.value) {
setupScrollListener()
}
return mockCartItems
})
const selectedCurrencyCode = ref<CurrencyCode>('TWD')
const isLoggedIn = computed(() => Boolean(userStore.userInfo))
const userPhoto = computed(() => userStore.memberData?.photo || '')
const userInitial = computed(() => userStore.userInfo?.nick?.[0] || '')
onBeforeUnmount(() => {
removeScrollListener()
})
const goPage = (path: string) => {
......
<template>
<header class="app-header flex flex-col items-center pt-[60px]">
<div class="fixed top-0 left-0 right-0 z-10 shadow-sm customPrimary-bg-7 flex justify-center">
<div class="fixed top-0 left-0 right-0 z-20 shadow-sm customPrimary-bg-7 flex justify-center">
<HeaderTopBar />
</div>
<div class="header-divider"></div>
<HeaderNavBar />
<HeaderNavBar v-if="showNavBar" />
</header>
</template>
<script setup lang="ts">
import HeaderTopBar from './HeaderTopBar.vue'
import HeaderNavBar from './HeaderNavBar.vue'
import { useRoute } from 'vue-router';
import { ref, watch } from 'vue';
const route = useRoute()
const showNavBar = ref(false)
const needNavs = ref<string[]>(['/','/home'])
watch(()=>route.path, () => {
console.log(route.path)
showNavBar.value = needNavs.value.includes(route.path)
})
showNavBar.value = needNavs.value.includes(route.path)
</script>
<style scoped lang="scss">
......
<template>
<div class="w-80 h-10">
<a-dropdown trigger="click" position="bl" class="!rounded-2xl">
<a-input-search
v-model="searchValue"
@blur="handleBlur"
@focus="handleFocus"
:class="hoverClass"
class="!w-full !h-full !border !rounded-full !text-[var(--customColor-text-10)]"
:placeholder="t('search.placeholder')"
@search="handleSearch"
/>
<!-- 下拉内容:参考 SearchBox 的布局,做轻量版;热门目的地固定两列 -->
<template #content>
<div
class="w-[640px] max-h-[540px] bg-white !rounded-2xl shadow-xl p-4 flex flex-col gap-4"
>
<!-- 热门搜索 -->
<div v-if="hotSearches.length" class="border-b pb-3">
<div class="text-xs font-semibold mb-3 text-[var(--customColor-text-10)]">
{{ t('search.hotSearch') }}
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="item in limitedHotSearches"
:key="item.id || item.keyword"
class="px-3 py-1 rounded-full text-xs border border-[var(--customPrimary-2)] text-[var(--customColor-text-9)] hover:bg-[var(--customPrimary-7)] hover:text-[var(--customPrimary-6)] transition-colors"
@click="handleSelectHotKeyword(item)"
>
{{ item.keyword }}
</button>
</div>
</div>
<!-- 热门目的地:固定两列 -->
<div v-if="mappedHotDestinations.length" class="flex-1 overflow-y-auto">
<div class="text-xs font-semibold mb-3 text-[var(--customColor-text-10)]">
{{ t('search.hotDestination') }}
</div>
<div class="grid grid-cols-2 gap-3">
<div
v-for="dest in mappedHotDestinations"
:key="dest.placeId"
class="flex items-center gap-3 p-2 rounded-xl cursor-pointer hover:bg-gray-100 transition-colors"
@click="handleSelectDestination(dest)"
>
<div class="w-12 h-12 rounded-lg overflow-hidden bg-[var(--customPrimary-2)] flex-shrink-0">
<img
v-if="dest.image"
:src="dest.image"
:alt="dest.name"
loading="lazy"
decoding="async"
class="w-full h-full object-cover"
/>
<div
v-else
class="w-full h-full flex items-center justify-center text-[var(--customColor-text-6)] text-xs"
>
{{ dest.name.charAt(0) }}
</div>
</div>
<div class="min-w-0">
<div class="text-sm font-medium text-[var(--customColor-text-10)] truncate">
{{ dest.name }}
</div>
<div class="text-xs text-[var(--customColor-text-7)] truncate">
{{ dest.subtitle }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</a-dropdown>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import HotKeywordService from '@/services/HotKeywordService'
import PlaceService from '@/services/PlaceService'
import { useSystemConfigStore } from '@/stores'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const systemConfigStore = useSystemConfigStore()
const { t } = useI18n()
const hotSearches = ref<any[]>([])
const hotDestinations = ref<any[]>([])
const searchValue = ref('')
// 输入框 hover / focus 样式(尽量复用灰阶和白色)
const hoverClass = ref(['!bg-gray-950/5', '!border-gray-950/10'])
const handleFocus = () => {
hoverClass.value = ['!bg-white']
}
const handleBlur = () => {
hoverClass.value = ['!bg-gray-950/5', '!border-gray-950/10']
}
const handleSearch = () => {
const keyword = searchValue.value.trim()
if (!keyword) return
// 这里暂时只打印,后续可接入实际搜索页
console.log('header search:', keyword)
}
// 热门搜索最多展示 10 个
const limitedHotSearches = computed(() => hotSearches.value.slice(0, 10))
// 将 PlaceService 返回的数据映射为下拉展示用的结构
const mappedHotDestinations = computed(() => {
return (hotDestinations.value || []).map((item: any) => {
const placeDetail = item.placeDetail || {}
const name = placeDetail.name || ''
const rawBg = placeDetail.backgroundImage as string | null | undefined
let image = ''
if (rawBg) {
try {
const parsed = JSON.parse(rawBg)
if (Array.isArray(parsed) && parsed.length > 0) {
image = parsed[0]
}
} catch {
const parts = rawBg.split(',').filter(Boolean)
image = parts[0] || ''
}
}
return {
placeId: item.placeId,
name,
subtitle: systemConfigStore.platformName || '',
image,
}
})
})
const handleSelectHotKeyword = (item: any) => {
searchValue.value = item.keyword
handleSearch()
}
const handleSelectDestination = (dest: { placeId: string; name: string }) => {
// 跳转到目的地详情页
router.push(`/place/${dest.placeId}`)
}
const getHotSearches = async () => {
try {
const resp = await HotKeywordService.getListAsync()
hotSearches.value = (resp as any).data ?? resp ?? []
} catch {
hotSearches.value = []
}
}
const getHotDestinations = async () => {
try {
const resp = await PlaceService.GetGlobalPlaceAsync(systemConfigStore.distributorId || 0)
// 后端结构:{ reList: GroupReDto[] }
hotDestinations.value = (resp as any).reList || []
} catch {
hotDestinations.value = []
}
}
onMounted(() => {
getHotSearches()
getHotDestinations()
})
</script>
<style scoped lang="scss">
</style>
\ No newline at end of file
......@@ -89,6 +89,12 @@ const router = createRouter({
meta: { title: "page.editEmail" },
component: () => import ('../views/personalCenter/accountPage/perForgePassword.vue')
},
{
path: '/place/:id',
name: 'placeDetail',
meta: { title: 'page.placeDetail' },
component: () => import ('../views/place/Detail.vue')
},
]
},
{
......
import Api, { type HttpResponse } from '@/api/OtaRequest';
export interface HotKeywordOutputDto {
id: string; // ID(UUID)
keyword: string | null; // 关键词
language: string | null; // 语言
enable: boolean; // 是否启用
orderNum: number; // 排序
creationTime: string; // 创建时间(ISO 8601格式)
lastModificationTime?: string | null; // 最后修改时间
}
/**
* 热门关键词服务类
*/
class HotKeywordService {
/**
* 获取列表(不分页)
* @param params 查询参数
* @returns 列表结果
*/
static async getListAsync(): Promise<HttpResponse<HotKeywordOutputDto[]>> {
return Api.get('/hot-keyword', {SkipCount: 0, MaxResultCount: 100,Enable: true });
}
}
export default HotKeywordService;
......@@ -58,6 +58,18 @@ export interface PlaceInputDto {
export interface PlaceOutputDto extends PlaceInputDto {
/** 主键 */
id: string
/** 国家名称 */
countryName?: string | null
/** 标签(JSON 或列表) */
tags?: string | null
/** 描述 */
description?: string | null
/** 英雄图(JSON 或列表) */
heroImages?: string | null
/** 天气提示 */
weatherHint?: string | null
/** 配置 JSON(运营钩子) */
config?: string | null
}
/**
......@@ -121,7 +133,113 @@ class PlaceService {
)
return response as unknown as PlaceGroupOutputDto[]
}
/**
* 获取目的地详情
* @param id 目的地ID
* @returns 目的地详情
*/
static async getPlaceDetailAsync(id: string): Promise<PlaceOutputDto> {
const response = await OtaRequest.get(
`/sys-management/place-services/${id}/detail`
)
return response as unknown as PlaceOutputDto
}
/**
* 获取目的地页面聚合数据
* @param id 目的地ID
* @returns 目的地页面聚合数据
*/
static async getPlacePageDataAsync(id: string): Promise<PlacePageDataOutputDto> {
const response = await OtaRequest.get(
`/sys-management/place-services/${id}/page-data`
)
return response as unknown as PlacePageDataOutputDto
}
/**
* 获取全局推荐的的目的地分组
* @param distributorId
* @returns
*/
static async GetGlobalPlaceAsync(
distributorId: number
): Promise<PlaceGroupOutputDto> {
const r = OtaRequest.get(`/sys-management/place-services/global-group/${distributorId}`);
return r as unknown as PlaceGroupOutputDto;
}
}
/**
* FAQ 相关接口
*/
export interface PlaceFaqDto {
id: string
placeId: string
question: string | null
answer: string | null
state: boolean
orderNum: number
}
/**
* 目的地页面聚合数据
*/
export interface PlacePageDataOutputDto {
destination: any
quickEntries?: QuickEntryDto[] | null
sections?: SectionDto[] | null
recommendations?: any[] | null
featuredReviews?: string[] | null
faqList?: string[] | null
weather?: any | null
}
export interface QuickEntryDto {
categoryId?: string | null
name?: string | null
count: number
link?: string | null
}
export interface SectionDto {
key?: string | null
title?: string | null
limit: number
items?: SectionItemDto[] | null
}
export interface SectionItemDto {
title?: string | null
price?: number | null
currency?: string | null
originPrice?: number | null
discount?: number | null
rating?: number | null
reviewsCount?: number | null
image?: string | null
link?: string | null
tags?: string[] | null
}
/**
* FAQ 服务
*/
class PlaceFaqService {
/**
* 根据目的地ID获取FAQ列表
* @param placeId 目的地ID
* @returns FAQ列表
*/
static async getFaqListByPlaceIdAsync(placeId: string): Promise<PlaceFaqDto[]> {
const response = await OtaRequest.get(
`/sys-management/place-faq-services/by-place/${placeId}`
)
return response as unknown as PlaceFaqDto[]
}
}
export { PlaceFaqService }
export default PlaceService
......@@ -27,6 +27,7 @@ export interface SearchBoxProps {
* 热门搜索配置
*/
hotSearches: {
dataMode: string
enabled: boolean
title: string // 标题,如 "KKday 熱門搜尋"
useDataSource: boolean // 是否使用数据源(false 则使用手动标签)
......@@ -40,6 +41,7 @@ export interface SearchBoxProps {
* 热门目的地配置
*/
hotDestinations: {
dataMode: string
enabled: boolean
title: string // 标题,如 "海外热门目的地"
selectedDestinations: string[] // 手动选择的目的地ID列表
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment