Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
B
boyueCEnd
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
游洁
boyueCEnd
Commits
18f5f06e
Commit
18f5f06e
authored
Dec 04, 2025
by
youjie
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
http://gitlab.oytour.com/youjie/boyuecend
parents
f9b600a1
af3e1ed6
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
285 additions
and
115 deletions
+285
-115
Card.vue
src/components/page-builder/Card.vue
+37
-5
Carousel.vue
src/components/page-builder/Carousel.vue
+127
-11
GridContainer.vue
src/components/page-builder/GridContainer.vue
+10
-0
SearchBox.vue
src/components/page-builder/SearchBox.vue
+1
-1
Footer.vue
src/layouts/components/Footer.vue
+77
-92
HeaderTopBar.vue
src/layouts/components/HeaderTopBar.vue
+1
-1
Headers.vue
src/layouts/components/Headers.vue
+4
-2
OTAPageRenderer.vue
src/renderer/OTAPageRenderer.vue
+15
-0
systemConfig.ts
src/stores/systemConfig.ts
+3
-1
card.ts
src/types/card.ts
+7
-1
carousel.ts
src/types/carousel.ts
+3
-1
No files found.
src/components/page-builder/Card.vue
View file @
18f5f06e
...
@@ -124,12 +124,44 @@ const cardStyle = computed(() => {
...
@@ -124,12 +124,44 @@ const cardStyle = computed(() => {
// 封面样式
// 封面样式
const
coverStyle
=
computed
(()
=>
{
const
coverStyle
=
computed
(()
=>
{
const
style
:
Record
<
string
,
string
>
=
{}
const
style
:
Record
<
string
,
string
>
=
{}
if
(
props
.
layout
===
'vertical'
)
{
// 获取尺寸模式(兼容旧数据)
style
.
height
=
`
${
props
.
coverHeight
}
px`
const
sizeMode
=
props
.
coverSizeMode
||
(
props
.
coverRatio
?
'ratio'
:
'custom'
)
if
(
sizeMode
===
'ratio'
&&
props
.
coverRatio
)
{
// 比例模式:使用 aspect-ratio CSS 属性
const
[
width
,
height
]
=
props
.
coverRatio
.
split
(
':'
).
map
(
Number
)
if
(
props
.
layout
===
'vertical'
)
{
// 纵向布局:宽度 100%,高度按比例计算
style
.
width
=
'100%'
style
.
aspectRatio
=
`
${
width
}
/
${
height
}
`
}
else
{
// 横向布局:高度 100%,宽度按比例计算
style
.
height
=
'100%'
style
.
aspectRatio
=
`
${
width
}
/
${
height
}
`
style
.
flexShrink
=
'0'
// 横向布局时,宽度由 aspect-ratio 自动计算,但需要设置一个基础宽度
style
.
minWidth
=
'0'
}
}
else
if
(
sizeMode
===
'custom'
)
{
// 自定义模式:使用自定义宽高
if
(
props
.
layout
===
'vertical'
)
{
style
.
width
=
props
.
coverCustomWidth
?
`
${
props
.
coverCustomWidth
}
px`
:
'100%'
style
.
height
=
props
.
coverCustomHeight
?
`
${
props
.
coverCustomHeight
}
px`
:
`
${
props
.
coverHeight
}
px`
}
else
{
style
.
width
=
props
.
coverCustomWidth
?
`
${
props
.
coverCustomWidth
}
px`
:
`
${
props
.
coverHeight
}
px`
style
.
height
=
props
.
coverCustomHeight
?
`
${
props
.
coverCustomHeight
}
px`
:
'100%'
style
.
flexShrink
=
'0'
}
}
else
{
}
else
{
style
.
width
=
`
${
props
.
coverHeight
}
px`
// 兼容旧数据:使用 coverHeight
style
.
flexShrink
=
'0'
if
(
props
.
layout
===
'vertical'
)
{
style
.
height
=
`
${
props
.
coverHeight
}
px`
}
else
{
style
.
width
=
`
${
props
.
coverHeight
}
px`
style
.
flexShrink
=
'0'
}
}
}
return
style
return
style
...
...
src/components/page-builder/Carousel.vue
View file @
18f5f06e
<
template
>
<
template
>
<div
class=
"carousel-wrapper"
:class=
"
{ 'outside-nav': isOutsideNav }" :style="wrapperStyle">
<div
class=
"carousel-wrapper"
:class=
"
{
'outside-nav': isOutsideNav
&&
!isAbsolutePosition,
'outside-nav-absolute': isOutsideNav
&&
isAbsolutePosition
}"
:style="wrapperStyle"
>
<!-- 外部左箭头 -->
<!-- 外部左箭头 -->
<template
v-if=
"isOutsideNav && props.navigation.enabled"
>
<template
v-if=
"isOutsideNav && props.navigation.enabled"
>
<div
class=
"outside-nav-btn outside-nav-prev"
@
click=
"handlePrevClick"
>
<div
class=
"outside-nav-btn outside-nav-prev"
:class=
"
{ 'absolute-position': isAbsolutePosition }"
:style="isAbsolutePosition ? outsideNavAbsoluteStyle('prev') : {}"
@click="handlePrevClick"
>
<!-- 默认模式 -->
<!-- 默认模式 -->
<template
v-if=
"props.navigation.arrowType === 'default'"
>
<template
v-if=
"props.navigation.arrowType === 'default'"
>
<svg
viewBox=
"0 0 24 24"
class=
"nav-svg"
>
<svg
viewBox=
"0 0 24 24"
class=
"nav-svg"
>
...
@@ -53,7 +65,7 @@
...
@@ -53,7 +65,7 @@
>
>
<!-- 图片轮播 -->
<!-- 图片轮播 -->
<SwiperSlide
v-for=
"(image, index) in props.images"
:key=
"image.id || index"
>
<SwiperSlide
v-for=
"(image, index) in props.images"
:key=
"image.id || index"
>
<div
class=
"carousel-slide"
@
click=
"handleImageClick(image)"
>
<div
class=
"carousel-slide"
:style=
"{borderRadius:props.borderRadius+'px'}"
@
click=
"handleImageClick(image)"
>
<!-- 有图片:显示图片 -->
<!-- 有图片:显示图片 -->
<
template
v-if=
"image.url"
>
<
template
v-if=
"image.url"
>
<div
class=
"slide-content"
>
<div
class=
"slide-content"
>
...
@@ -63,18 +75,27 @@
...
@@ -63,18 +75,27 @@
:style="imageStyle"
:style="imageStyle"
loading="lazy"
loading="lazy"
class="slide-image"
class="slide-image"
v-if="!image.content?.showOnHover"
/>
/>
<div
:style=
"
{...imageStyle,backgroundImage: `url(${image.url})`}" v-else class="slide-hover-image">
</div>
<!-- 遮罩层 -->
<!-- 遮罩层 -->
<div
v-if=
"overlayStyle"
class=
"slide-overlay"
:style=
"overlayStyle"
></div>
<div
v-if=
"overlayStyle
&& !image.content?.showOnHover
"
class=
"slide-overlay"
:style=
"overlayStyle"
></div>
</div>
</div>
<!-- 内容层(文案或子组件) -->
<!-- 内容层(文案或子组件) -->
<div
<div
class=
"slide-content-layer"
class=
"slide-content-layer"
:class=
"
{'hover-layer': image.content?.showOnHover}"
:style="contentLayerStyle"
:style="contentLayerStyle"
@mouseenter="handleSlideMouseEnter(index)"
@mouseleave="handleSlideMouseLeave(index)"
>
>
<!-- 优先显示图片自己的文案 -->
<!-- 优先显示图片自己的文案 -->
<template
v-if=
"image.content && (image.content.title || image.content.subtitle || image.content.description || image.content.buttonText)"
>
<template
v-if=
"image.content && (image.content.title || image.content.subtitle || image.content.description || image.content.buttonText)"
>
<div
class=
"slide-text-content"
>
<div
class=
"slide-text-content"
:class=
"
{ 'show-on-hover': image.content.showOnHover }"
>
<h2
v-if=
"image.content.title"
class=
"slide-title"
:style=
"titleStyle"
>
<h2
v-if=
"image.content.title"
class=
"slide-title"
:style=
"titleStyle"
>
{{
image
.
content
.
title
}}
{{
image
.
content
.
title
}}
</h2>
</h2>
...
@@ -172,7 +193,12 @@
...
@@ -172,7 +193,12 @@
</div>
</div>
<!-- 外部右箭头 -->
<!-- 外部右箭头 -->
<
template
v-if=
"isOutsideNav && props.navigation.enabled"
>
<
template
v-if=
"isOutsideNav && props.navigation.enabled"
>
<div
class=
"outside-nav-btn outside-nav-next"
@
click=
"handleNextClick"
>
<div
class=
"outside-nav-btn outside-nav-next"
:class=
"
{ 'absolute-position': isAbsolutePosition }"
:style="isAbsolutePosition ? outsideNavAbsoluteStyle('next') : {}"
@click="handleNextClick"
>
<!-- 默认模式 -->
<!-- 默认模式 -->
<template
v-if=
"props.navigation.arrowType === 'default'"
>
<template
v-if=
"props.navigation.arrowType === 'default'"
>
<svg
viewBox=
"0 0 24 24"
class=
"nav-svg"
>
<svg
viewBox=
"0 0 24 24"
class=
"nav-svg"
>
...
@@ -356,6 +382,40 @@ const isOutsideNav = computed(() => {
...
@@ -356,6 +382,40 @@ const isOutsideNav = computed(() => {
return
props
.
navigation
.
enabled
&&
props
.
navigation
.
position
===
'outside'
return
props
.
navigation
.
enabled
&&
props
.
navigation
.
position
===
'outside'
})
})
// 判断外部箭头是否使用绝对定位(不占用组件宽度)
const
isAbsolutePosition
=
computed
(()
=>
{
return
isOutsideNav
.
value
&&
(
props
.
navigation
.
absolutePosition
===
true
)
})
// 处理幻灯片鼠标进入(用于其他可能的交互)
const
handleSlideMouseEnter
=
(
index
:
number
)
=>
{
// 可以在这里添加其他悬浮交互逻辑
}
// 处理幻灯片鼠标离开(用于其他可能的交互)
const
handleSlideMouseLeave
=
(
index
:
number
)
=>
{
// 可以在这里添加其他悬浮交互逻辑
}
// 外部箭头绝对定位样式
const
outsideNavAbsoluteStyle
=
(
direction
:
'prev'
|
'next'
)
=>
{
const
{
size
,
offset
}
=
props
.
navigation
const
style
:
Record
<
string
,
string
>
=
{
position
:
'absolute'
,
top
:
'50%'
,
transform
:
'translateY(-50%)'
,
zIndex
:
'10'
,
}
if
(
direction
===
'prev'
)
{
style
.
left
=
`-
${
size
+
offset
}
px`
}
else
{
style
.
right
=
`-
${
size
+
offset
}
px`
}
return
style
}
// 导航配置
// 导航配置
const
navigationConfig
=
computed
(()
=>
{
const
navigationConfig
=
computed
(()
=>
{
if
(
!
props
.
navigation
.
enabled
)
return
false
if
(
!
props
.
navigation
.
enabled
)
return
false
...
@@ -521,7 +581,7 @@ const wrapperStyle = computed(() => {
...
@@ -521,7 +581,7 @@ const wrapperStyle = computed(() => {
style
.
width
=
'100%'
style
.
width
=
'100%'
style
.
marginLeft
=
'auto'
style
.
marginLeft
=
'auto'
style
.
marginRight
=
'auto'
style
.
marginRight
=
'auto'
style
.
padding
=
'0
2
0px'
// 两侧留白
style
.
padding
=
'0 0px'
// 两侧留白
style
.
boxSizing
=
'border-box'
style
.
boxSizing
=
'border-box'
}
}
}
else
{
}
else
{
...
@@ -598,6 +658,7 @@ const imageStyle = computed<CSSProperties>(() => ({
...
@@ -598,6 +658,7 @@ const imageStyle = computed<CSSProperties>(() => ({
height
:
'100%'
,
height
:
'100%'
,
objectFit
:
'cover'
,
objectFit
:
'cover'
,
display
:
'block'
,
display
:
'block'
,
borderRadius
:
props
.
borderRadius
+
'px'
,
}))
}))
// 处理图片点击
// 处理图片点击
...
@@ -952,6 +1013,7 @@ const contentLayerStyle = computed(() => {
...
@@ -952,6 +1013,7 @@ const contentLayerStyle = computed(() => {
const
{
horizontal
,
vertical
}
=
props
.
contentAlign
||
{
horizontal
:
'center'
,
vertical
:
'middle'
}
const
{
horizontal
,
vertical
}
=
props
.
contentAlign
||
{
horizontal
:
'center'
,
vertical
:
'middle'
}
return
{
return
{
borderRadius
:
props
.
borderRadius
+
'px'
,
// flex-direction: column 时,justifyContent 控制垂直对齐,alignItems 控制水平对齐
// flex-direction: column 时,justifyContent 控制垂直对齐,alignItems 控制水平对齐
justifyContent
:
vertical
===
'top'
?
'flex-start'
:
vertical
===
'bottom'
?
'flex-end'
:
'center'
,
justifyContent
:
vertical
===
'top'
?
'flex-start'
:
vertical
===
'bottom'
?
'flex-end'
:
'center'
,
alignItems
:
horizontal
===
'left'
?
'flex-start'
:
horizontal
===
'right'
?
'flex-end'
:
'center'
,
alignItems
:
horizontal
===
'left'
?
'flex-start'
:
horizontal
===
'right'
?
'flex-end'
:
'center'
,
...
@@ -980,11 +1042,11 @@ const descriptionStyle = computed(() => ({
...
@@ -980,11 +1042,11 @@ const descriptionStyle = computed(() => ({
}))
}))
const buttonStyle = computed(() => ({
const buttonStyle = computed(() => ({
backgroundColor: props.contentStyle?.button.backgroundColor || '
#ffffff
',
backgroundColor: props.contentStyle?.button.backgroundColor || '
transparent
',
color: props.contentStyle?.button.color || '#1d2129',
color: props.contentStyle?.button.color || '#1d2129',
borderRadius: `
$
{(
props
.
contentStyle
?.
button
.
borderRadius
||
4
)
/
16
}
rem
`,
borderRadius: `
$
{(
props
.
contentStyle
?.
button
.
borderRadius
||
4
)
/
16
}
rem
`,
padding: `
$
{(
props
.
contentStyle
?.
button
.
paddingVertical
||
8
)
/
16
}
rem
$
{(
props
.
contentStyle
?.
button
.
paddingHorizontal
||
24
)
/
16
}
rem
`,
padding: `
$
{(
props
.
contentStyle
?.
button
.
paddingVertical
)
/
16
}
rem
$
{(
props
.
contentStyle
?.
button
.
paddingHorizontal
)
/
16
}
rem
`,
fontSize: `
$
{(
props
.
contentStyle
?.
button
.
fontSize
||
1
4
)
/
16
}
rem
`,
fontSize: `
$
{(
props
.
contentStyle
?.
button
.
fontSize
||
1
2
)
/
16
}
rem
`,
fontWeight: props.contentStyle?.button.fontWeight || 500,
fontWeight: props.contentStyle?.button.fontWeight || 500,
border: 'none',
border: 'none',
cursor: 'pointer',
cursor: 'pointer',
...
@@ -1009,7 +1071,26 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1009,7 +1071,26 @@ const handleButtonClick = (buttonLink?: string) => {
position
:
relative
;
position
:
relative
;
z-index
:
9
;
z-index
:
9
;
}
}
// 父容器悬浮时显示
.swiper-slide
{
&
:hover
{
.show-on-hover
{
opacity
:
1
;
visibility
:
visible
;
transition
:
.3s
;
}
.slide-hover-image
{
filter
:
blur
(
6px
);
transform
:
scale
(
1
.05
);
transition
:
.3s
;
}
.hover-layer
{
background
:
rgba
(
0
,
0
,
0
,
.3
);
transition
:
.3s
;
overflow
:
hidden
;
}
}
}
.carousel-swiper
{
.carousel-swiper
{
width
:
100%
;
width
:
100%
;
...
@@ -1084,6 +1165,15 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1084,6 +1165,15 @@ const handleButtonClick = (buttonLink?: string) => {
}
}
}
}
// === 外部模式:绝对定位(不占用宽度) ===
.carousel-wrapper.outside-nav-absolute
{
position
:
relative
;
.carousel-swiper
{
width
:
100%
;
}
}
// === 外部箭头按钮 ===
// === 外部箭头按钮 ===
.outside-nav-btn
{
.outside-nav-btn
{
flex-shrink
:
0
;
flex-shrink
:
0
;
...
@@ -1100,6 +1190,14 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1100,6 +1190,14 @@ const handleButtonClick = (buttonLink?: string) => {
user-select
:
none
;
user-select
:
none
;
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0
.15
);
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0
.15
);
// 绝对定位模式
&
.absolute-position
{
position
:
absolute
;
top
:
50%
;
transform
:
translateY
(
-50%
);
z-index
:
10
;
}
// SVG 样式(70%)
// SVG 样式(70%)
.nav-svg
{
.nav-svg
{
width
:
v-bind
(
'`${props.navigation.size * 0.7}px`'
);
width
:
v-bind
(
'`${props.navigation.size * 0.7}px`'
);
...
@@ -1237,6 +1335,7 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1237,6 +1335,7 @@ const handleButtonClick = (buttonLink?: string) => {
height
:
100%
;
height
:
100%
;
position
:
relative
;
position
:
relative
;
cursor
:
pointer
;
cursor
:
pointer
;
overflow
:
hidden
;
img
{
img
{
user-select
:
none
;
user-select
:
none
;
...
@@ -1285,6 +1384,14 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1285,6 +1384,14 @@ const handleButtonClick = (buttonLink?: string) => {
object-fit
:
cover
;
object-fit
:
cover
;
display
:
block
;
display
:
block
;
}
}
.slide-hover-image
{
width
:
100%
;
height
:
100%
;
display
:
block
;
background-size
:
cover
;
background-position
:
center
;
background-repeat
:
no-repeat
;
}
// Ken Burns 缩放效果
// Ken Burns 缩放效果
.carousel-swiper
{
.carousel-swiper
{
...
@@ -1332,6 +1439,7 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1332,6 +1439,7 @@ const handleButtonClick = (buttonLink?: string) => {
>
*
{
>
*
{
pointer-events
:
auto
;
pointer-events
:
auto
;
}
}
}
}
// 文案内容容器
// 文案内容容器
...
@@ -1341,6 +1449,14 @@ const handleButtonClick = (buttonLink?: string) => {
...
@@ -1341,6 +1449,14 @@ const handleButtonClick = (buttonLink?: string) => {
align-items
:
inherit
;
// 继承父容器的对齐
align-items
:
inherit
;
// 继承父容器的对齐
text-align
:
center
;
text-align
:
center
;
max-width
:
800px
;
// 限制最大宽度,提高可读性
max-width
:
800px
;
// 限制最大宽度,提高可读性
transition
:
opacity
0
.3s
ease
,
visibility
0
.3s
ease
;
// 悬浮显示模式
&
.show-on-hover
{
opacity
:
0
;
visibility
:
hidden
;
}
.slide-title
,
.slide-title
,
.slide-subtitle
,
.slide-subtitle
,
...
...
src/components/page-builder/GridContainer.vue
View file @
18f5f06e
...
@@ -92,8 +92,18 @@ const gridCells = computed(() => {
...
@@ -92,8 +92,18 @@ const gridCells = computed(() => {
return
[]
return
[]
})
})
// 计算当前应该显示的列数(考虑响应式)
// 计算当前应该显示的列数(考虑响应式)
const
currentColumns
=
computed
(()
=>
{
const
currentColumns
=
computed
(()
=>
{
// 如果响应式配置启用
if
(
props
.
responsive
?.
enabled
&&
props
.
responsive
?.
config
)
{
const
responsiveCols
=
props
.
responsive
.
config
[
runtimeDevice
.
value
]
// 如果响应式列数不为0,优先使用响应式列数
if
(
responsiveCols
&&
responsiveCols
>
0
)
{
return
responsiveCols
}
}
// 否则使用默认的 columns
return
props
.
columns
return
props
.
columns
})
})
...
...
src/components/page-builder/SearchBox.vue
View file @
18f5f06e
...
@@ -259,7 +259,7 @@ const buttonStyleComputed = computed(() => {
...
@@ -259,7 +259,7 @@ const buttonStyleComputed = computed(() => {
// 下拉面板样式
// 下拉面板样式
const
dropdownStyleComputed
=
computed
(()
=>
({
const
dropdownStyleComputed
=
computed
(()
=>
({
maxHeight
:
`
${
props
.
dropdownStyle
.
maxHeight
}
px`
,
maxHeight
:
props
.
dropdownStyle
.
maxHeight
==
0
?
"unset"
:
`
${
props
.
dropdownStyle
.
maxHeight
}
px`
,
borderRadius
:
`
${
props
.
dropdownStyle
.
borderRadius
}
px`
,
borderRadius
:
`
${
props
.
dropdownStyle
.
borderRadius
}
px`
,
backgroundColor
:
props
.
dropdownStyle
.
backgroundColor
,
backgroundColor
:
props
.
dropdownStyle
.
backgroundColor
,
boxShadow
:
props
.
dropdownStyle
.
boxShadow
,
boxShadow
:
props
.
dropdownStyle
.
boxShadow
,
...
...
src/layouts/components/Footer.vue
View file @
18f5f06e
...
@@ -15,7 +15,13 @@
...
@@ -15,7 +15,13 @@
class=
"social-icon hover:opacity-80 transition-opacity"
class=
"social-icon hover:opacity-80 transition-opacity"
:title=
"t(`footer.social.$
{key}`)"
:title=
"t(`footer.social.$
{key}`)"
>
>
<span
class=
"text-lg"
>
{{
getSocialIcon
(
key
)
}}
</span>
<VIcon
v-if=
"getSocialIconName(key)"
:name=
"getSocialIconName(key)!"
fill=
"outline"
class=
"text-[24px]"
/>
<span
v-else
class=
"text-sm"
>
{{
key
}}
</span>
</a>
</a>
</div>
</div>
...
@@ -51,47 +57,14 @@
...
@@ -51,47 +57,14 @@
<!-- 中间区域:链接列 -->
<!-- 中间区域:链接列 -->
<div
class=
"footer-middle border-b border-gray-700 py-8"
>
<div
class=
"footer-middle border-b border-gray-700 py-8"
>
<div
class=
"footer-container"
>
<div
class=
"footer-container"
>
<div
class=
"grid grid-cols-1 md:grid-cols-4 gap-8"
>
<div
class=
"grid grid-cols-1 gap-8"
:class=
"[`md:grid-cols-$
{bottomNavs.length+1}`]">
<!-- 关于我们 -->
<div>
<h3
class=
"font-semibold mb-4"
>
{{
t
(
'footer.about.title'
)
}}
</h3>
<ul
class=
"space-y-2"
>
<li
v-for=
"link in aboutLinks"
:key=
"link.key"
>
<a
:href=
"link.url"
class=
"text-sm text-gray-300 hover:text-white transition-colors"
>
{{
t
(
`footer.about.${link.key
}
`
)
}}
<
/a
>
<
/li
>
<
/ul
>
<
/div
>
<!--
给旅人
-->
<
div
>
<
h3
class
=
"font-semibold mb-4"
>
{{
t
(
'footer.travelers.title'
)
}}
<
/h3
>
<
ul
class
=
"space-y-2"
>
<
li
v
-
for
=
"link in travelerLinks"
:
key
=
"link.key"
>
<
a
:
href
=
"link.url"
class
=
"text-sm text-gray-300 hover:text-white transition-colors"
>
{{
t
(
`footer.travelers.${link.key
}
`
)
}}
<
/a
>
<
/li
>
<
/ul
>
<
/div
>
<!--
合作伙伴
-->
<div
v-for=
"item in bottomNavs"
:key=
"item.id"
>
<
div
>
<h3
class=
"font-semibold mb-4"
>
{{
t
(
item
.
navTitle
)
}}
</h3>
<
h3
class
=
"font-semibold mb-4"
>
{{
t
(
'footer.partners.title'
)
}}
<
/h3
>
<ul
class=
"space-y-2"
>
<ul
class=
"space-y-2"
>
<
li
v
-
for
=
"link in partnerLinks"
:
key
=
"link.key"
>
<li
v-for=
"child in item.childList"
:key=
"child.id"
>
<
a
<a
:href=
"child.navUrl??'javascript:void(0);'"
class=
"text-sm text-gray-300 hover:text-white transition-colors"
>
:
href
=
"link.url"
{{
t
(
child
.
navTitle
)
}}
class
=
"text-sm text-gray-300 hover:text-white transition-colors"
>
{{
t
(
`footer.partners.${link.key
}
`
)
}}
</a>
</a>
</li>
</li>
</ul>
</ul>
...
@@ -110,10 +83,9 @@
...
@@ -110,10 +83,9 @@
:name=
"method.icon"
:name=
"method.icon"
fill=
"outline"
fill=
"outline"
class=
"text-[20px]"
class=
"text-[20px]"
v-if=
"method.icon && method.icon!=''"
/>
/>
<
span
class
=
"text-xs text-gray-200"
>
<img
:src=
"method.url"
:alt=
"method.label"
class=
"w-[42px] h-[auto]"
v-if=
"method.url && method.url!=''"
/>
{{
method
.
label
}}
<
/span
>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -155,84 +127,97 @@ import { useI18n } from 'vue-i18n'
...
@@ -155,84 +127,97 @@ import { useI18n } from 'vue-i18n'
import
{
useSystemConfigStore
}
from
'@/stores/systemConfig'
import
{
useSystemConfigStore
}
from
'@/stores/systemConfig'
import
{
IconUp
}
from
'@arco-design/web-vue/es/icon'
import
{
IconUp
}
from
'@arco-design/web-vue/es/icon'
import
{
Message
}
from
'@arco-design/web-vue'
import
{
Message
}
from
'@arco-design/web-vue'
import
VIcon
from
'@/components/global/VIcon.vue'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
systemConfigStore
=
useSystemConfigStore
()
const
systemConfigStore
=
useSystemConfigStore
()
const
email
=
ref
(
''
)
const
email
=
ref
(
''
)
const
bottomNavs
=
computed
(()
=>
systemConfigStore
.
getBottomNavs
)
// 社交媒体字段到 VIcon 图标名称的映射
const
socialIconMap
:
Record
<
string
,
string
>
=
{
facebook
:
'facebook'
,
instagram
:
'instagram'
,
snapchat
:
'snapchat'
,
whatsapp
:
'whatsapp'
,
youtube
:
'youtube'
,
twitter
:
'twitter'
,
tiktok
:
'tiktok'
,
}
// 社交媒体链接
// 所有支持的社交媒体字段
const
allSocialFields
=
[
'facebook'
,
'instagram'
,
'kakaotalk'
,
'line'
,
'linkedin'
,
'messenger'
,
'naver'
,
'signal'
,
'snapchat'
,
'telegram'
,
'tiktok'
,
'twitter'
,
'viber'
,
'wechat'
,
'weibo'
,
'whatsapp'
,
'xiaohongshu'
,
'youtube'
,
'zalo'
]
// 社交媒体链接(自动过滤空值)
const
socialMediaLinks
=
computed
(()
=>
{
const
socialMediaLinks
=
computed
(()
=>
{
const
config
=
systemConfigStore
.
config
const
config
=
systemConfigStore
.
config
if
(
!
config
)
return
{}
if
(
!
config
)
return
{}
const
links
:
Record
<
string
,
string
>
=
{}
const
links
:
Record
<
string
,
string
>
=
{}
const
socialFields
=
[
'facebook'
,
'youtube'
,
'instagram'
,
'weibo'
,
'twitter'
,
'pinterest'
]
s
ocialFields
.
forEach
(
field
=>
{
allS
ocialFields
.
forEach
(
field
=>
{
const
value
=
(
config
as
any
)[
field
]
const
value
=
(
config
as
any
)[
field
]
if
(
value
&&
typeof
value
===
'string'
)
{
// 只添加非空字符串链接
links
[
field
]
=
value
if
(
value
&&
typeof
value
===
'string'
&&
value
.
trim
()
!==
''
)
{
links
[
field
]
=
value
.
trim
()
}
}
})
})
return
links
return
links
})
})
// 获取社交媒体图标组件
// 获取社交媒体图标名称(如果支持)
const
getSocialIcon
=
(
key
:
string
)
=>
{
const
getSocialIconName
=
(
key
:
string
):
string
|
null
=>
{
// 使用简单的文本图标,实际项目中可以使用图标库
return
socialIconMap
[
key
]
||
null
const
iconMap
:
Record
<
string
,
string
>
=
{
facebook
:
'f'
,
youtube
:
'▶'
,
instagram
:
'📷'
,
weibo
:
'微博'
,
twitter
:
'X'
,
pinterest
:
'P'
}
return
iconMap
[
key
]
||
key
}
}
// 关于我们链接
// 汇总没有图标的媒体(用于调试,可在控制台查看)
const
aboutLinks
=
[
// 使用 console.log 输出,避免未使用变量的警告
{
key
:
'about'
,
url
:
'/about'
}
,
if
(
import
.
meta
.
env
.
DEV
)
{
{
key
:
'terms'
,
url
:
'/terms'
}
,
const
checkSocialMediaWithoutIcons
=
()
=>
{
{
key
:
'privacy'
,
url
:
'/privacy'
}
,
const
config
=
systemConfigStore
.
config
{
key
:
'help'
,
url
:
'/help'
}
,
if
(
!
config
)
return
{
key
:
'media'
,
url
:
'/media'
}
]
const
withoutIcons
:
string
[]
=
[]
// 给旅人链接
allSocialFields
.
forEach
(
field
=>
{
const
travelerLinks
=
[
const
value
=
(
config
as
any
)[
field
]
{
key
:
'guarantee'
,
url
:
'/guarantee'
}
,
// 如果有值但没有对应的图标
{
key
:
'blog'
,
url
:
'/blog'
}
,
if
(
value
&&
typeof
value
===
'string'
&&
value
.
trim
()
!==
''
&&
!
socialIconMap
[
field
])
{
{
key
:
'points'
,
url
:
'/points'
}
,
withoutIcons
.
push
(
field
)
{
key
:
'redeem'
,
url
:
'/redeem'
}
,
}
{
key
:
'pointsCard'
,
url
:
'/points-card'
}
})
]
if
(
withoutIcons
.
length
>
0
)
{
console
.
log
(
'[Footer] 没有图标的社交媒体:'
,
withoutIcons
)
}
}
// 延迟检查,确保 config 已加载
setTimeout
(
checkSocialMediaWithoutIcons
,
1000
)
}
// 合作伙伴链接
const
partnerLinks
=
[
{
key
:
'becomeSupplier'
,
url
:
'/become-supplier'
}
,
{
key
:
'supplierLogin'
,
url
:
'/supplier-login'
}
,
{
key
:
'rezio'
,
url
:
'/rezio'
}
,
{
key
:
'cooperation'
,
url
:
'/cooperation'
}
,
{
key
:
'affiliate'
,
url
:
'/affiliate'
}
]
// 支付方式图标(使用 Keenicons,通过全局组件 VIcon 渲染)
// 支付方式图标(使用 Keenicons,通过全局组件 VIcon 渲染)
const
paymentIcons
=
[
const
paymentIcons
=
[
{
key
:
'card'
,
icon
:
'two-credit-cart'
,
label
:
'Credit Card'
}
,
{
key
:
'visa'
,
icon
:
''
,
label
:
'Visa'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/visa.svg'
},
{
key
:
'paypal'
,
icon
:
'paypal'
,
label
:
'PayPal'
}
,
{
key
:
'mastercard'
,
icon
:
''
,
label
:
'MasterCard'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/mastercard.svg'
},
{
key
:
'apple'
,
icon
:
'apple'
,
label
:
'Apple Pay'
}
,
{
key
:
'jcb'
,
icon
:
''
,
label
:
'JCB'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/jcb.svg'
},
{
key
:
'google'
,
icon
:
'google-play'
,
label
:
'Google Pay'
}
,
{
key
:
'jko_pay'
,
icon
:
''
,
label
:
'JKO Pay'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/jko_pay.svg'
},
{
key
:
'apple_pay'
,
icon
:
''
,
label
:
'Apple Pay'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/apple_pay.svg'
},
{
key
:
'google_pay'
,
icon
:
''
,
label
:
'Google Pay'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/google_pay.svg'
},
{
key
:
'line_pay'
,
icon
:
''
,
label
:
'Line Pay'
,
url
:
'https://cdn.kkday.com/pc-web/assets/img/footer/payment/line_pay.svg'
},
]
]
// 版权信息
// 版权信息
const
copyright
=
computed
(()
=>
{
const
copyright
=
computed
(()
=>
{
return
systemConfigStore
.
copyright
||
`COPYRIGHT © ${new Date().getFullYear()
}
All rights reserved.`
return
systemConfigStore
.
platformConfig
?.
copyright
||
`COPYRIGHT ©
${
new
Date
().
getFullYear
()}
All rights reserved.`
})
})
// 订阅 Newsletter
// 订阅 Newsletter
...
...
src/layouts/components/HeaderTopBar.vue
View file @
18f5f06e
<
template
>
<
template
>
<div
class=
"w-[1200px] py-2 flex items-center justify-between h-[60px] text-xs text-gray-700"
>
<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"
>
<div
class=
"flex items-center gap-4"
>
<img
<img
v-if=
"logo"
v-if=
"logo"
...
...
src/layouts/components/Headers.vue
View file @
18f5f06e
<
template
>
<
template
>
<header
class=
"app-header shadow-sm customPrimary-bg-7 flex flex-col items-center"
>
<header
class=
"app-header flex flex-col items-center pt-[60px]"
>
<HeaderTopBar
/>
<div
class=
"fixed top-0 left-0 right-[14px] z-10 shadow-sm customPrimary-bg-7 flex justify-center"
>
<HeaderTopBar
/>
</div>
<div
class=
"h-[1px] w-full bg-gray-700/5"
></div>
<div
class=
"h-[1px] w-full bg-gray-700/5"
></div>
<HeaderNavBar
/>
<HeaderNavBar
/>
</header>
</header>
...
...
src/renderer/OTAPageRenderer.vue
View file @
18f5f06e
...
@@ -16,6 +16,21 @@
...
@@ -16,6 +16,21 @@
/>
/>
</div>
</div>
<!-- GridContainer 特殊处理:需要传递 children 到 slot -->
<div
v-else-if=
"comp.type === 'grid-container'"
:style=
"getComponentWrapperStyle(comp)"
>
<component
:is=
"getComponent(comp.type)"
v-bind=
"comp.props"
>
<component
v-for=
"child in comp.children"
:key=
"child.id"
:is=
"getComponent(child.type)"
v-bind=
"child.props"
/>
</component>
</div>
<!-- 其他组件:应用 maxWidth 样式 -->
<!-- 其他组件:应用 maxWidth 样式 -->
<div
v-else
:style=
"getComponentWrapperStyle(comp)"
>
<div
v-else
:style=
"getComponentWrapperStyle(comp)"
>
<component
<component
...
...
src/stores/systemConfig.ts
View file @
18f5f06e
...
@@ -152,7 +152,9 @@ export const useSystemConfigStore = defineStore('systemConfig', {
...
@@ -152,7 +152,9 @@ export const useSystemConfigStore = defineStore('systemConfig', {
getTopNavs
:
(
state
):
NavItemDto
[]
=>
{
getTopNavs
:
(
state
):
NavItemDto
[]
=>
{
return
state
.
navs
?.
filter
(
item
=>
item
.
type
===
1
)
||
[]
return
state
.
navs
?.
filter
(
item
=>
item
.
type
===
1
)
||
[]
},
},
getBottomNavs
:
(
state
):
NavItemDto
[]
=>
{
return
state
.
navs
?.
filter
(
item
=>
item
.
type
===
2
)
||
[]
},
},
},
actions
:
{
actions
:
{
/**
/**
...
...
src/types/card.ts
View file @
18f5f06e
...
@@ -8,9 +8,15 @@ export interface CardProps {
...
@@ -8,9 +8,15 @@ export interface CardProps {
// 封面图片
// 封面图片
coverImage
?:
string
// 封面图片 URL
coverImage
?:
string
// 封面图片 URL
coverHeight
:
number
// 封面高度(px,纵向布局)或宽度(横向布局)
coverHeight
:
number
// 封面高度(px,纵向布局)或宽度(横向布局)
- 兼容旧数据
coverFit
:
'cover'
|
'contain'
|
'fill'
// 图片适配方式
coverFit
:
'cover'
|
'contain'
|
'fill'
// 图片适配方式
// 封面尺寸模式(新增)
coverSizeMode
?:
'ratio'
|
'custom'
// 尺寸模式:比例/自定义(默认 'ratio' 兼容旧数据)
coverRatio
?:
string
// 预设比例,如 '16:9', '4:3', '1:1' 等
coverCustomWidth
?:
number
// 自定义宽度(px,仅在 custom 模式下使用)
coverCustomHeight
?:
number
// 自定义高度(px,仅在 custom 模式下使用)
// 标题区
// 标题区
title
:
string
// 主标题
title
:
string
// 主标题
titleSize
:
number
// 标题字号
titleSize
:
number
// 标题字号
...
...
src/types/carousel.ts
View file @
18f5f06e
...
@@ -13,9 +13,10 @@ export interface CarouselImage {
...
@@ -13,9 +13,10 @@ export interface CarouselImage {
content
?:
{
content
?:
{
title
?:
string
// 标题
title
?:
string
// 标题
subtitle
?:
string
// 副标题
subtitle
?:
string
// 副标题
description
?:
string
// 描述
description
?:
string
// 描述
buttonText
?:
string
// 按钮文字
buttonText
?:
string
// 按钮文字
buttonLink
?:
string
// 按钮链接
buttonLink
?:
string
// 按钮链接
showOnHover
?:
boolean
// 是否仅在悬浮时显示(默认 false,始终显示)
}
}
}
}
...
@@ -106,6 +107,7 @@ export interface CarouselProps {
...
@@ -106,6 +107,7 @@ export interface CarouselProps {
// 位置
// 位置
position
:
'inside'
|
'outside'
// 内部/外部
position
:
'inside'
|
'outside'
// 内部/外部
offset
:
number
// 距离边缘的偏移 0-50px
offset
:
number
// 距离边缘的偏移 0-50px
absolutePosition
?:
boolean
// 外部箭头是否使用绝对定位(不占用组件宽度),默认 false
}
}
// 分页指示器(简化,删除透明度选项)
// 分页指示器(简化,删除透明度选项)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment