Commit 18f5f06e authored by youjie's avatar youjie
parents f9b600a1 af3e1ed6
......@@ -124,12 +124,44 @@ const cardStyle = computed(() => {
// 封面样式
const coverStyle = computed(() => {
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 {
style.width = `${props.coverHeight}px`
style.flexShrink = '0'
// 兼容旧数据:使用 coverHeight
if (props.layout === 'vertical') {
style.height = `${props.coverHeight}px`
} else {
style.width = `${props.coverHeight}px`
style.flexShrink = '0'
}
}
return style
......
<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">
<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'">
<svg viewBox="0 0 24 24" class="nav-svg">
......@@ -53,7 +65,7 @@
>
<!-- 图片轮播 -->
<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">
<div class="slide-content">
......@@ -63,18 +75,27 @@
:style="imageStyle"
loading="lazy"
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
class="slide-content-layer"
:class="{'hover-layer': image.content?.showOnHover}"
: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)">
<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">
{{ image.content.title }}
</h2>
......@@ -172,7 +193,12 @@
</div>
<!-- 外部右箭头 -->
<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'">
<svg viewBox="0 0 24 24" class="nav-svg">
......@@ -356,6 +382,40 @@ const isOutsideNav = computed(() => {
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(() => {
if (!props.navigation.enabled) return false
......@@ -521,7 +581,7 @@ const wrapperStyle = computed(() => {
style.width = '100%'
style.marginLeft = 'auto'
style.marginRight = 'auto'
style.padding = '0 20px' // 两侧留白
style.padding = '0 0px' // 两侧留白
style.boxSizing = 'border-box'
}
} else {
......@@ -598,6 +658,7 @@ const imageStyle = computed<CSSProperties>(() => ({
height: '100%',
objectFit: 'cover',
display: 'block',
borderRadius:props.borderRadius + 'px',
}))
// 处理图片点击
......@@ -952,6 +1013,7 @@ const contentLayerStyle = computed(() => {
const { horizontal, vertical } = props.contentAlign || { horizontal: 'center', vertical: 'middle' }
return {
borderRadius:props.borderRadius + 'px',
// flex-direction: column 时,justifyContent 控制垂直对齐,alignItems 控制水平对齐
justifyContent: vertical === 'top' ? 'flex-start' : vertical === 'bottom' ? 'flex-end' : 'center',
alignItems: horizontal === 'left' ? 'flex-start' : horizontal === 'right' ? 'flex-end' : 'center',
......@@ -980,11 +1042,11 @@ const descriptionStyle = computed(() => ({
}))
const buttonStyle = computed(() => ({
backgroundColor: props.contentStyle?.button.backgroundColor || '#ffffff',
backgroundColor: props.contentStyle?.button.backgroundColor || 'transparent',
color: props.contentStyle?.button.color || '#1d2129',
borderRadius: `${(props.contentStyle?.button.borderRadius || 4) / 16}rem`,
padding: `${(props.contentStyle?.button.paddingVertical || 8) / 16}rem ${(props.contentStyle?.button.paddingHorizontal || 24) / 16}rem`,
fontSize: `${(props.contentStyle?.button.fontSize || 14) / 16}rem`,
padding: `${(props.contentStyle?.button.paddingVertical) / 16}rem ${(props.contentStyle?.button.paddingHorizontal) / 16}rem`,
fontSize: `${(props.contentStyle?.button.fontSize || 12) / 16}rem`,
fontWeight: props.contentStyle?.button.fontWeight || 500,
border: 'none',
cursor: 'pointer',
......@@ -1009,7 +1071,26 @@ const handleButtonClick = (buttonLink?: string) => {
position: relative;
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 {
width: 100%;
......@@ -1084,6 +1165,15 @@ const handleButtonClick = (buttonLink?: string) => {
}
}
// === 外部模式:绝对定位(不占用宽度) ===
.carousel-wrapper.outside-nav-absolute {
position: relative;
.carousel-swiper {
width: 100%;
}
}
// === 外部箭头按钮 ===
.outside-nav-btn {
flex-shrink: 0;
......@@ -1100,6 +1190,14 @@ const handleButtonClick = (buttonLink?: string) => {
user-select: none;
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%)
.nav-svg {
width: v-bind('`${props.navigation.size * 0.7}px`');
......@@ -1237,6 +1335,7 @@ const handleButtonClick = (buttonLink?: string) => {
height: 100%;
position: relative;
cursor: pointer;
overflow: hidden;
img {
user-select: none;
......@@ -1285,6 +1384,14 @@ const handleButtonClick = (buttonLink?: string) => {
object-fit: cover;
display: block;
}
.slide-hover-image {
width: 100%;
height: 100%;
display: block;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
// Ken Burns 缩放效果
.carousel-swiper {
......@@ -1332,6 +1439,7 @@ const handleButtonClick = (buttonLink?: string) => {
> * {
pointer-events: auto;
}
}
// 文案内容容器
......@@ -1341,6 +1449,14 @@ const handleButtonClick = (buttonLink?: string) => {
align-items: inherit; // 继承父容器的对齐
text-align: center;
max-width: 800px; // 限制最大宽度,提高可读性
transition: opacity 0.3s ease, visibility 0.3s ease;
// 悬浮显示模式
&.show-on-hover {
opacity: 0;
visibility: hidden;
}
.slide-title,
.slide-subtitle,
......
......@@ -92,8 +92,18 @@ const gridCells = computed(() => {
return []
})
// 计算当前应该显示的列数(考虑响应式)
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
})
......
......@@ -259,7 +259,7 @@ const buttonStyleComputed = computed(() => {
// 下拉面板样式
const dropdownStyleComputed = computed(() => ({
maxHeight: `${props.dropdownStyle.maxHeight}px`,
maxHeight: props.dropdownStyle.maxHeight==0?"unset":`${props.dropdownStyle.maxHeight}px`,
borderRadius: `${props.dropdownStyle.borderRadius}px`,
backgroundColor: props.dropdownStyle.backgroundColor,
boxShadow: props.dropdownStyle.boxShadow,
......
......@@ -15,7 +15,13 @@
class="social-icon hover:opacity-80 transition-opacity"
: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>
</div>
......@@ -51,47 +57,14 @@
<!-- 中间区域:链接列 -->
<div class="footer-middle border-b border-gray-700 py-8">
<div class="footer-container">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- 关于我们 -->
<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 class="grid grid-cols-1 gap-8" :class="[`md:grid-cols-${bottomNavs.length+1}`]">
<!-- 合作伙伴 -->
<div>
<h3 class="font-semibold mb-4">{{ t('footer.partners.title') }}</h3>
<div v-for="item in bottomNavs" :key="item.id">
<h3 class="font-semibold mb-4">{{ t(item.navTitle) }}</h3>
<ul class="space-y-2">
<li v-for="link in partnerLinks" :key="link.key">
<a
:href="link.url"
class="text-sm text-gray-300 hover:text-white transition-colors"
>
{{ t(`footer.partners.${link.key}`) }}
<li v-for="child in item.childList" :key="child.id">
<a :href="child.navUrl??'javascript:void(0);'" class="text-sm text-gray-300 hover:text-white transition-colors">
{{ t(child.navTitle) }}
</a>
</li>
</ul>
......@@ -110,10 +83,9 @@
:name="method.icon"
fill="outline"
class="text-[20px]"
v-if="method.icon && method.icon!=''"
/>
<span class="text-xs text-gray-200">
{{ method.label }}
</span>
<img :src="method.url" :alt="method.label" class="w-[42px] h-[auto]" v-if="method.url && method.url!=''" />
</div>
</div>
</div>
......@@ -155,84 +127,97 @@ import { useI18n } from 'vue-i18n'
import { useSystemConfigStore } from '@/stores/systemConfig'
import { IconUp } from '@arco-design/web-vue/es/icon'
import { Message } from '@arco-design/web-vue'
import VIcon from '@/components/global/VIcon.vue'
const { t } = useI18n()
const systemConfigStore = useSystemConfigStore()
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 config = systemConfigStore.config
if (!config) return {}
const links: Record<string, string> = {}
const socialFields = [
'facebook', 'youtube', 'instagram', 'weibo', 'twitter', 'pinterest'
]
socialFields.forEach(field => {
allSocialFields.forEach(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
})
// 获取社交媒体图标组件
const getSocialIcon = (key: string) => {
// 使用简单的文本图标,实际项目中可以使用图标库
const iconMap: Record<string, string> = {
facebook: 'f',
youtube: '▶',
instagram: '📷',
weibo: '微博',
twitter: 'X',
pinterest: 'P'
}
return iconMap[key] || key
// 获取社交媒体图标名称(如果支持)
const getSocialIconName = (key: string): string | null => {
return socialIconMap[key] || null
}
// 关于我们链接
const aboutLinks = [
{ key: 'about', url: '/about' },
{ key: 'terms', url: '/terms' },
{ key: 'privacy', url: '/privacy' },
{ key: 'help', url: '/help' },
{ key: 'media', url: '/media' }
]
// 给旅人链接
const travelerLinks = [
{ key: 'guarantee', url: '/guarantee' },
{ key: 'blog', url: '/blog' },
{ key: 'points', url: '/points' },
{ key: 'redeem', url: '/redeem' },
{ key: 'pointsCard', url: '/points-card' }
]
// 汇总没有图标的媒体(用于调试,可在控制台查看)
// 使用 console.log 输出,避免未使用变量的警告
if (import.meta.env.DEV) {
const checkSocialMediaWithoutIcons = () => {
const config = systemConfigStore.config
if (!config) return
const withoutIcons: string[] = []
allSocialFields.forEach(field => {
const value = (config as any)[field]
// 如果有值但没有对应的图标
if (value && typeof value === 'string' && value.trim() !== '' && !socialIconMap[field]) {
withoutIcons.push(field)
}
})
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 渲染)
const paymentIcons = [
{ key: 'card', icon: 'two-credit-cart', label: 'Credit Card' },
{ key: 'paypal', icon: 'paypal', label: 'PayPal' },
{ key: 'apple', icon: 'apple', label: 'Apple Pay' },
{ key: 'google', icon: 'google-play', label: 'Google Pay' },
{key:'visa',icon:'',label:'Visa',url:'https://cdn.kkday.com/pc-web/assets/img/footer/payment/visa.svg'},
{key:'mastercard',icon:'',label:'MasterCard',url:'https://cdn.kkday.com/pc-web/assets/img/footer/payment/mastercard.svg'},
{key:'jcb',icon:'',label:'JCB',url:'https://cdn.kkday.com/pc-web/assets/img/footer/payment/jcb.svg'},
{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(() => {
return systemConfigStore.copyright || `COPYRIGHT © ${new Date().getFullYear()} All rights reserved.`
return systemConfigStore.platformConfig?.copyright || `COPYRIGHT © ${new Date().getFullYear()} All rights reserved.`
})
// 订阅 Newsletter
......
<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">
<img
v-if="logo"
......
<template>
<header class="app-header shadow-sm customPrimary-bg-7 flex flex-col items-center">
<HeaderTopBar />
<header class="app-header flex flex-col items-center pt-[60px]">
<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>
<HeaderNavBar />
</header>
......
......@@ -16,6 +16,21 @@
/>
</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 样式 -->
<div v-else :style="getComponentWrapperStyle(comp)">
<component
......
......@@ -152,7 +152,9 @@ export const useSystemConfigStore = defineStore('systemConfig', {
getTopNavs: (state): NavItemDto[] => {
return state.navs?.filter(item => item.type === 1) || []
},
getBottomNavs: (state): NavItemDto[] => {
return state.navs?.filter(item => item.type === 2) || []
},
},
actions: {
/**
......
......@@ -8,9 +8,15 @@ export interface CardProps {
// 封面图片
coverImage?: string // 封面图片 URL
coverHeight: number // 封面高度(px,纵向布局)或宽度(横向布局)
coverHeight: number // 封面高度(px,纵向布局)或宽度(横向布局)- 兼容旧数据
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 // 主标题
titleSize: number // 标题字号
......
......@@ -13,9 +13,10 @@ export interface CarouselImage {
content?: {
title?: string // 标题
subtitle?: string // 副标题
description?: string // 描述
description?: string // 描述
buttonText?: string // 按钮文字
buttonLink?: string // 按钮链接
showOnHover?: boolean // 是否仅在悬浮时显示(默认 false,始终显示)
}
}
......@@ -106,6 +107,7 @@ export interface CarouselProps {
// 位置
position: 'inside' | 'outside' // 内部/外部
offset: number // 距离边缘的偏移 0-50px
absolutePosition?: boolean // 外部箭头是否使用绝对定位(不占用组件宽度),默认 false
}
// 分页指示器(简化,删除透明度选项)
......
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