Browse Source

feat: 增加动效

main
betaqi 3 months ago
parent
commit
266e4698a1
  1. 56
      components/RadiantText.vue
  2. 37
      components/SwiperSlider3D.vue
  3. 89
      components/TextHighlight.vue
  4. 17
      components/pageLayout/cloudData.vue
  5. 2
      components/pageLayout/electricPower.vue
  6. 93
      components/pageLayout/midele.vue
  7. 8
      components/pageLayout/serce.vue
  8. 192
      components/ui/glowing-effect/GlowingEffect.vue
  9. 1
      components/ui/glowing-effect/index.ts
  10. 49
      components/ui/letter-pullup/LetterPullup.vue
  11. 1
      components/ui/letter-pullup/index.ts
  12. 116
      components/ui/sparkles-text/SparklesText.vue
  13. 1
      components/ui/sparkles-text/index.ts
  14. 55
      components/ui/text-generate-effect/TextGenerateEffect.vue
  15. 1
      components/ui/text-generate-effect/index.ts
  16. 3
      components/ui/vortex/Vortex.vue
  17. 1
      components/ui/vortex/index.ts
  18. 11
      layouts/default.vue
  19. 7
      nuxt.config.ts
  20. 6
      package.json
  21. 57
      pages/index.vue
  22. 168
      pages/products/BK-1000.vue
  23. 168
      pages/products/BK-2000.vue
  24. 85
      pages/text.vue
  25. 924
      pnpm-lock.yaml
  26. BIN
      public/images/aboutUs/bg.png
  27. BIN
      public/images/banner/bk1000.png
  28. BIN
      public/images/banner/bk1000_1x.png
  29. BIN
      public/images/banner/bk1000_2x.png
  30. BIN
      public/images/banner/bk2000.png
  31. BIN
      public/images/banner/bk2000_1x.png
  32. BIN
      public/images/banner/bk2000_2x.png
  33. BIN
      public/images/product/BK-1000_lg.png
  34. BIN
      public/images/product/BK-1000_sm.png
  35. BIN
      public/images/product/BK-2000_lg.png
  36. BIN
      public/images/product/Bk-2000_sm.png

56
components/RadiantText.vue

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
<template>
<p
:style="styleVar"
:class="
cn(
'radiant-animation bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent ',
$props.class,
)
"
>
<slot />
</p>
</template>
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { computed } from 'vue'
const props = defineProps({
duration: {
type: Number,
default: 1,
},
radiantWidth: {
type: Number,
default: 500,
},
class: String,
})
const styleVar = computed(() => {
return {
'--radiant-anim-duration': `${props.duration}s`,
'--radiant-width': `${props.radiantWidth}px`,
}
})
</script>
<style scoped>
@keyframes radiant {
0%,
90%,
100% {
background-position: calc(-100% - var(--radiant-width)) 0;
}
30%,
60% {
background-position: calc(100% + var(--radiant-width)) 0;
}
}
.radiant-animation {
animation: radiant var(--radiant-anim-duration) infinite;
}
</style>

37
components/SwiperSlider3D.vue

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
<template>
<ClientOnly>
<div class="carousel-container">
<swiper-container ref="containerRef" :init="false">
<swiper-container ref="containerRef" :init="false" :effect="'coverflow'">
<slot></slot>
</swiper-container>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</ClientOnly>
</template>
@ -11,17 +13,17 @@ @@ -11,17 +13,17 @@
<script setup>
const containerRef = ref(null)
useSwiper(containerRef, {
effect: 'coverflow',
grabCursor: true,
centeredSlides: true,
loop: true,
initialSlide: 2,
slidesPerView: 'auto',
effect: 'coverflow', // 3d
grabCursor: true, //
centeredSlides: true, //
loop: true, //
initialSlide: 2, //
slidesPerView: 'auto', //
coverflowEffect: {
rotate: 0,
stretch: 0,
depth: 100,
modifier: 2.5,
rotate: 0, //
stretch: 0, //
depth: 100, //
modifier: 2.5, //
},
pagination: {
el: '.swiper-pagination',
@ -41,4 +43,17 @@ useSwiper(containerRef, { @@ -41,4 +43,17 @@ useSwiper(containerRef, {
overflow: hidden;
height: 100%;
}
.swiper-button-prev,
.swiper-button-next {
@apply absolute top-0 bottom-0 w-32 z-10;
}
.swiper-button-prev {
left: 0px;
}
.swiper-button-next {
right: 0px;
}
</style>

89
components/TextHighlight.vue

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
<template>
<span
v-if="!props.highlightText"
:class="cn('inline-block px-1 pb-1 highlight-all', props.class)"
>
{{ props.text }}
</span>
<span v-else>
{{ beforeText }}
<div :class="cn('inline-block highlight-all', props.class)">
{{ props.highlightText }}
</div>
{{ afterText }}
</span>
</template>
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
interface Props {
delay?: number
duration?: number
class?: HTMLAttributes['class']
textEndColor?: string
highlightText?: string
text: string
}
const props = withDefaults(defineProps<Props>(), {
delay: 0,
duration: 2000,
endColor: 'inherit',
})
const delayMs = computed(() => `${props.delay}ms`)
const durationMs = computed(() => `${props.duration}ms`)
const beforeText = computed(() => {
if (!props.highlightText || !props.text) return ''
const index = props.text.indexOf(props.highlightText)
if (index === -1) return props.text
return props.text.substring(0, index)
})
const afterText = computed(() => {
if (!props.highlightText || !props.text) return ''
const index = props.text.indexOf(props.highlightText)
if (index === -1) return ''
return props.text.substring(index + props.highlightText.length)
})
</script>
<style scoped>
@keyframes background-expand {
0% {
background-size: 0% 100%;
}
100% {
background-size: 100% 100%;
}
}
@keyframes text-color-change {
0% {
color: inherit;
}
100% {
color: v-bind(textEndColor);
}
}
span {
background-size: 0% 100%;
background-repeat: no-repeat;
background-position: left center;
}
.highlight-all,
.highlight-text {
animation:
background-expand v-bind(durationMs) ease-in-out v-bind(delayMs) forwards,
text-color-change v-bind(durationMs) ease-in-out v-bind(delayMs) forwards;
}
</style>

17
components/pageLayout/cloudData.vue

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
>
<h2 class="title-font py-11 md:py-8 sm:py-4">云端数据</h2>
<div
class="h-[580px] max-w-[1400px] md:h-[382px] md:w-[760px] sm:w-[348px] sm:h-[176px] mx-auto"
class="relative h-[580px] max-w-[1400px] md:h-[382px] md:w-[760px] sm:w-[348px] sm:h-[176px] mx-auto"
>
<ClientOnly>
<MovieCarousel3D>
@ -15,19 +15,6 @@ @@ -15,19 +15,6 @@
</div>
</swiper-slide>
</MovieCarousel3D>
<!-- <swiper-container
ref="containerRef"
class="size-full mx-auto hidden sm:block md:block"
:autoplay="{ delay: 3000 }"
loop
pagination
>
<swiper-slide v-for="(movie, index) in imgList" :key="index" class="size-full">
<div class="w-full h-full">
<img :src="movie" alt="" class="w-full h-full object-cover" />
</div>
</swiper-slide>
</swiper-container> -->
</ClientOnly>
</div>
<div
@ -49,7 +36,7 @@ @@ -49,7 +36,7 @@
<script setup>
import MovieCarousel3D from '~/components/SwiperSlider3D.vue'
const imgList = ['/images/cloudData/7m.png', '/images/cloudData/7m.png', '/images/cloudData/7m.png']
const imgList = ['/images/cloudData/7l.png', '/images/cloudData/7m.png', '/images/cloudData/7r.png']
const cloudDataList = [
{
icon: '/images/cloudData/7icon1.png',

2
components/pageLayout/electricPower.vue

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<template>
<div class="relative mt-17 md:mt-6 sm:mt-3 pb-[92px] md:pb-[32px] sm:pb-[16px]">
<div class="relative md:mt-6 sm:mt-3 pb-[92px] md:pb-[32px] sm:pb-[16px]">
<h2 class="title-font pt-12 pb-10 md:pb-7 sm:pt-[16px] sm:pb-[12px] relative z-10">
电力四级防护
</h2>

93
components/pageLayout/midele.vue

@ -1,46 +1,48 @@ @@ -1,46 +1,48 @@
<template>
<h2 class="title-font pt-20 sm:p-4 md:p-8 pb-19">全生态中间件</h2>
<div
class="max-w-[1440px] mx-auto md:p-5 bg-black relative flex items-center justify-center pb-[107px] md:pb-17 sm:pb-10"
ref="containerRef"
>
<Vortex class="relative pb-17">
<h2 class="title-font pt-20 sm:p-4 md:p-8 pb-19">全生态中间件</h2>
<div
v-for="(item, idx) in middleWareList"
:key="idx"
class="relative shadow-[0_0_4px_0_rgba(7,6,44,0.61)] rounded-xl bg-cover overflow-hidden transition-all duration-300 cursor-pointer"
:class="[
item.clx,
{
'scale-active': activeIndex === idx,
'scale-inactive': activeIndex !== null && activeIndex !== idx && idx === 1,
'scale-default': activeIndex === null && idx === 1,
},
]"
@mouseenter="setActiveIndex(idx)"
@mouseleave="clearActiveIndex()"
class="max-w-[1440px] mx-auto md:p-5 bg-transparent relative flex items-center justify-center pb-[107px] md:pb-17 sm:pb-10"
ref="containerRef"
>
<picture class="absolute inset-0 z-0 text-white">
<img :src="item.lgBg" loading="eager" class="size-full" :alt="item.title" />
</picture>
<section class="relative z-10 sm:p-3 size-full flex flex-col">
<h3
class="text-[#5897FF] text-5xl md:text-2xl md:mt-6 sm:text-xs mt-15 sm:mt-3 text-center font-bold"
>
{{ item.title }}
</h3>
<div
class="text-[#A4B0B7] flex flex-col mt-21 md:mt-6 sm:mt-3 items-center text-3xl md:text-base sm:text-[8px] sm:leading-[14px] gap-9 md:gap-4 sm:gap-2"
>
<p v-for="(content, idx) in item.contentList" :key="idx" class="text-center">
{{ content }}
</p>
</div>
</section>
<div
v-for="(item, idx) in middleWareList"
:key="idx"
class="relative shadow-[0_0_4px_0_rgba(7,6,44,0.61)] rounded-xl bg-cover overflow-hidden transition-all duration-300 cursor-pointer"
:class="[
item.clx,
{
'scale-active': activeIndex === idx,
'scale-default': activeIndex === null && idx === 1,
},
]"
@mouseenter="setActiveIndex(idx)"
@mouseleave="clearActiveIndex()"
>
<picture class="absolute inset-0 z-0 text-white">
<img :src="item.lgBg" loading="eager" class="size-full" :alt="item.title" />
</picture>
<section class="relative z-10 sm:p-3 size-full flex flex-col">
<h3
class="text-[#5897FF] text-4xl md:text-2xl md:mt-6 sm:text-xs mt-15 sm:mt-3 text-center font-bold"
>
{{ item.title }}
</h3>
<div
class="text-[#A4B0B7] flex flex-col mt-21 md:mt-6 sm:mt-3 items-center text-3xl md:text-base sm:text-[8px] sm:leading-[14px] gap-9 md:gap-4 sm:gap-2"
>
<p v-for="(content, idx) in item.contentList" :key="idx" class="text-center">
{{ content }}
</p>
</div>
</section>
</div>
</div>
</div>
</Vortex>
</template>
<script setup lang="ts">
import Vortex from '@/components/ui/vortex/Vortex.vue'
const activeIndex = ref<number | null>(null)
const setActiveIndex = (idx: number) => {
@ -89,23 +91,4 @@ const middleWareList = [ @@ -89,23 +91,4 @@ const middleWareList = [
transform: scale(1.1);
z-index: 30;
}
.scale-inactive {
width: 540px !important;
height: 584px !important;
}
@media (max-width: 768px) {
.scale-inactive {
width: 234px !important;
height: 288px !important;
}
}
@media (max-width: 640px) {
.scale-inactive {
width: 126px !important;
height: 152px !important;
}
}
</style>

8
components/pageLayout/serce.vue

@ -2,6 +2,13 @@ @@ -2,6 +2,13 @@
<h2 class="title-font pt-12 pb-10 md:pb-7 sm:pt-[16px] sm:pb-[12px]">服务矩阵</h2>
<div class="grid grid-cols-8 gap-[24px] max-w-[1440px] mx-auto sm:hidden md:hidden">
<div v-for="(item, idx) in serveList" :key="idx" :class="item.clx">
<GlowingEffect
:spread="40"
:glow="true"
:disabled="false"
:proximity="64"
:inactive-zone="0.01"
/>
<picture class="absolute inset-0 z-0">
<source media="(max-width: 480px)" :srcset="item.smBg" />
<source media="(max-width: 1280px)" :srcset="item.lgBg" />
@ -49,6 +56,7 @@ @@ -49,6 +56,7 @@
</template>
<script setup lang="ts">
import GlowingEffect from '@/components/ui/glowing-effect/GlowingEffect.vue'
const serveList = [
{
title: '丰富的接口',

192
components/ui/glowing-effect/GlowingEffect.vue

@ -0,0 +1,192 @@ @@ -0,0 +1,192 @@
<template>
<div
:class="
cn(
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
glow && 'opacity-100',
variant === 'white' && 'border-white',
disabled && '!block',
)
"
/>
<div
ref="containerRef"
:style="containerStyles"
:class="
cn(
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
glow && 'opacity-100',
blur > 0 && 'blur-[var(--blur)]',
props.class,
disabled && '!hidden',
)
"
>
<div
:class="
cn(
'glow',
'rounded-[inherit]',
`after:content-[''] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]`,
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
'after:[background:var(--gradient)] after:[background-attachment:fixed]',
'after:opacity-[var(--active)] after:transition-opacity after:duration-300',
'after:[mask-clip:padding-box,border-box]',
'after:[mask-composite:intersect]',
'after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
)
"
/>
</div>
</template>
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { templateRef } from '@vueuse/core'
import { animate } from 'motion-v'
import type { HTMLAttributes } from 'vue'
interface Props {
blur?: number
inactiveZone?: number
proximity?: number
spread?: number
variant?: 'default' | 'white'
glow?: boolean
class?: HTMLAttributes['class']
disabled?: boolean
movementDuration?: number
borderWidth?: number
}
const props = withDefaults(defineProps<Props>(), {
blur: 0,
inactiveZone: 0.7,
proximity: 0,
spread: 20,
variant: 'default',
glow: false,
movementDuration: 2,
borderWidth: 1,
disabled: true,
})
const containerRef = templateRef('containerRef')
const lastPosition = ref({
x: 0,
y: 0,
})
const animationFrame = ref(0)
const containerStyles = computed(() => {
return {
'--blur': `${props.blur}px`,
'--spread': props.spread,
'--start': '0',
'--active': '0',
'--glowingeffect-border-width': `${props.borderWidth}px`,
'--repeating-conic-gradient-times': '5',
'--gradient':
props.variant === 'white'
? `repeating-conic-gradient(
from 236.84deg at 50% 50%,
var(--black),
var(--black) calc(25% / var(--repeating-conic-gradient-times))
)`
: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
repeating-conic-gradient(
from 236.84deg at 50% 50%,
#dd7bbb 0%,
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
#dd7bbb calc(100% / var(--repeating-conic-gradient-times))
)`,
}
})
onMounted(() => {
if (props.disabled) return
window.addEventListener('scroll', handleScroll, { passive: true })
document.body.addEventListener('pointermove', handlePointerMove, {
passive: true,
})
})
onUnmounted(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
}
window.removeEventListener('scroll', handleScroll)
document.body.removeEventListener('pointermove', handlePointerMove)
})
function handlePointerMove(e: PointerEvent) {
handleMove(e)
}
function handleScroll() {
handleMove()
}
function handleMove(e?: MouseEvent | PointerEvent | { x: number; y: number }) {
if (!containerRef.value) return
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
}
animationFrame.value = requestAnimationFrame(() => {
const element = containerRef.value
if (!element) return
const { left, top, width, height } = element.getBoundingClientRect()
const mouseX = e?.x ?? lastPosition.value.x
const mouseY = e?.y ?? lastPosition.value.y
if (e) {
lastPosition.value = { x: mouseX, y: mouseY }
}
const center = [left + width * 0.5, top + height * 0.5]
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1])
const inactiveRadius = 0.5 * Math.min(width, height) * props.inactiveZone
if (distanceFromCenter < inactiveRadius) {
element.style.setProperty('--active', '0')
return
}
const isActive =
mouseX > left - props.proximity &&
mouseX < left + width + props.proximity &&
mouseY > top - props.proximity &&
mouseY < top + height + props.proximity
element.style.setProperty('--active', isActive ? '1' : '0')
if (!isActive) return
const currentAngle = parseFloat(element.style.getPropertyValue('--start')) || 0
let targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180
const newAngle = currentAngle + angleDiff
animate(currentAngle, newAngle, {
duration: props.movementDuration,
ease: [0.16, 1, 0.3, 1],
onUpdate: value => {
element.style.setProperty('--start', String(value))
},
})
})
}
</script>

1
components/ui/glowing-effect/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export { default as GlowingEffect } from './GlowingEffect.vue';

49
components/ui/letter-pullup/LetterPullup.vue

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
<template>
<div class="flex justify-center">
<div
v-for="(letter, index) in letters"
:key="letter"
>
<Motion
as="h1"
:initial="pullupVariant.initial"
:animate="pullupVariant.animate"
:transition="{
delay: index * (props.delay ? props.delay : 0.05),
}"
:class="
cn(
'font-display text-center text-4xl font-bold tracking-[-0.02em] text-black drop-shadow-sm md:text-4xl md:leading-[5rem]',
props.class,
)
"
>
<span v-if="letter === ' '">&nbsp;</span>
<span v-else>{{ letter }}</span>
</Motion>
</div>
</div>
</template>
<script setup lang="ts">
import { Motion } from 'motion-v';
import { cn } from '@/lib/utils';
interface LetterPullupProps {
class?: string;
words: string;
delay?: number;
}
const props = defineProps<LetterPullupProps>();
const letters = props.words.split('');
const pullupVariant = {
initial: { y: 100, opacity: 0 },
animate: {
y: 0,
opacity: 1,
},
};
</script>

1
components/ui/letter-pullup/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export { default as LetterPullup } from './LetterPullup.vue';

116
components/ui/sparkles-text/SparklesText.vue

@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
<template>
<div :class="props.class">
<span class="relative inline-block">
<template v-if="isClientMounted" v-for="sparkle in sparkles" :key="sparkle.id">
<motion.svg
:initial="{ opacity: 0, scale: 0, rotate: 75 }"
:animate="{
opacity: [0, 1, 0],
scale: [0, sparkle.scale, 0],
rotate: [75, 120, 150],
}"
:transition="{
duration: 0.8,
repeat: Infinity,
delay: sparkle.delay,
}"
class="pointer-events-none absolute z-20"
:style="{
left: sparkle.x,
top: sparkle.y,
opacity: 0,
}"
width="21"
height="21"
viewBox="0 0 21 21"
>
<path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
:fill="sparkle.color"
/>
</motion.svg>
</template>
{{ text }}
</span>
</div>
</template>
<script setup lang="ts">
import { motion } from 'motion-v'
import { ref, onMounted, onUnmounted } from 'vue'
interface Sparkle {
id: string
x: string
y: string
color: string
delay: number
scale: number
lifespan: number
}
interface Props {
text: string
sparklesCount?: number
colors?: {
first: string
second: string
}
class?: string
}
const props = withDefaults(defineProps<Props>(), {
sparklesCount: 6,
colors: () => ({ first: '#9E7AFF', second: '#FE8BBB' }),
})
const sparkles = ref<Sparkle[]>([])
const isClientMounted = ref(false)
// Generate a new sparkle with randomized properties
function generateStar(): Sparkle {
const starX = `${Math.random() * 100}%`
const starY = `${Math.random() * 100}%`
const color = Math.random() > 0.5 ? props.colors.first : props.colors.second
const delay = Math.random() * 2
const scale = Math.random() * 1 + 0.3
const lifespan = Math.random() * 10 + 5
const id = `${starX}-${starY}-${Date.now()}`
return { id, x: starX, y: starY, color, delay, scale, lifespan }
}
// Initialize sparkles array with random stars
function initializeStars() {
sparkles.value = Array.from({ length: props.sparklesCount }, generateStar)
}
// Update sparkles - regenerate dead ones and update lifespans
function updateStars() {
//
sparkles.value = sparkles.value.map(star => {
if (star.lifespan <= 0) {
return generateStar()
} else {
return { ...star, lifespan: star.lifespan - 0.1 }
}
})
}
let interval: number | undefined
// Start animation loop
onMounted(() => {
isClientMounted.value = true
initializeStars()
if (typeof window !== 'undefined') {
interval = window.setInterval(updateStars, 100) // 100ms
}
})
// Cleanup on unmount
onUnmounted(() => {
if (interval) {
clearInterval(interval)
}
})
</script>

1
components/ui/sparkles-text/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export { default as SparklesText } from './SparklesText.vue';

55
components/ui/text-generate-effect/TextGenerateEffect.vue

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
<template>
<div :class="cn('leading-snug tracking-wide', props.class)">
<div ref="scope">
<span
v-for="(word, idx) in wordsArray"
:key="word + idx"
class="inline-block"
:style="spanStyle"
>
{{ word }}&nbsp;
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, type HTMLAttributes, onMounted, ref } from 'vue';
import { cn } from '@/lib/utils';
const props = withDefaults(
defineProps<{
words: string;
filter?: boolean;
duration?: number;
delay?: number;
class: HTMLAttributes["class"];
}>(),
{ duration: 0.7, delay: 0, filter: true },
);
const scope = ref(null);
const wordsArray = computed(() => props.words.split(' '));
const spanStyle = computed(() => ({
opacity: 0,
filter: props.filter ? 'blur(10px)' : 'none',
transition: `opacity ${props.duration}s, filter ${props.duration}s`,
}));
onMounted(() => {
if (scope.value) {
const spans = (scope.value as HTMLElement).querySelectorAll('span');
setTimeout(() => {
spans.forEach((span: HTMLElement, index: number) => {
setTimeout(() => {
span.style.opacity = '1';
span.style.filter = props.filter ? 'blur(0px)' : 'none';
}, index * 200);
});
}, props.delay);
}
});
</script>

1
components/ui/text-generate-effect/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export { default as TextGenerateEffect } from './TextGenerateEffect.vue';

3
components/Vortex.vue → components/ui/vortex/Vortex.vue

@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
:animate="{ opacity: 1 }"
class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent"
>
<canvas ref="canvasRef"></canvas>
<canvas ref="canvasRef" class="size-full"></canvas>
</Motion>
<div :class="cn('relative z-10', props.class)">
@ -21,6 +21,7 @@ import { createNoise3D } from 'simplex-noise' @@ -21,6 +21,7 @@ import { createNoise3D } from 'simplex-noise'
import { onMounted, onUnmounted } from 'vue'
import { templateRef, useDebounceFn } from '@vueuse/core'
import { cn } from '@/lib/utils'
const TAU = 2 * Math.PI
const BASE_TTL = 50
const RANGE_TTL = 150

1
components/ui/vortex/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export { default as Vortex } from './Vortex.vue';

11
layouts/default.vue

@ -5,10 +5,13 @@ @@ -5,10 +5,13 @@
>
<div
class="max-w-[1920px] mx-auto flex h-17 sm:h-13 w-full items-center justify-between px-28 sm:px-6 md:px-[88px] peer"
v-if="!updateStateNavbar"
>
<h1 class="text-3xl md:text-2xl sm:text-base font-bold text-[#52AC63] relative z-20">
<NuxtLink to="/">BTDK</NuxtLink>
<h1 class="text-3xl md:text-2xl sm:text-base font-bold relative z-20">
<NuxtLink to="/"> <LetterPullup
words="BTDK"
:delay="0.05"
class="text-[#52AC63]""
/> </NuxtLink>
</h1>
<nav class="absolute inset-0">
<ul
@ -169,7 +172,7 @@ @@ -169,7 +172,7 @@
<script setup lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import LetterPullup from '@/components/ui/letter-pullup/LetterPullup.vue'
const isScrolled = ref(false)
const router = useRouter()
const checkScroll = () => {

7
nuxt.config.ts

@ -5,6 +5,7 @@ export default defineNuxtConfig({ @@ -5,6 +5,7 @@ export default defineNuxtConfig({
'shadcn-nuxt',
'@nuxt/icon',
'nuxt-swiper',
'motion-v/nuxt',
],
devtools: { enabled: true },
postcss: {
@ -17,6 +18,12 @@ export default defineNuxtConfig({ @@ -17,6 +18,12 @@ export default defineNuxtConfig({
vite: {
plugins: [
],
optimizeDeps: {
include: ['motion-v'],
},
ssr: {
noExternal: ['motion-v'],
},
},
app: {
head: {

6
package.json

@ -11,10 +11,10 @@ @@ -11,10 +11,10 @@
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@vueuse/core": "^13.0.0",
"@vueuse/core": "^13.1.0",
"class-variance-authority": "^0.7.1",
"lucide-vue-next": "^0.486.0",
"motion-v": "1.0.0-alpha.1",
"motion-v": "1.0.0-beta.2",
"nuxt": "^3.16.1",
"nuxt-swiper": "2.0.0",
"reka-ui": "^2.2.0",
@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwind-merge": "^3.1.0",
"tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7"
},
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"

57
pages/index.vue

@ -1,24 +1,21 @@ @@ -1,24 +1,21 @@
<template>
<main class="w-full bg-black lg:min-w-[1200px]">
<main class="w-full bg-[#0C0C16] lg:min-w-[1200px]">
<div
class="relative h-[428px] w-full bg-gradient-to-b from-[#0C0C16] to-[#0C131E] lg:h-[888px]"
class="relative h-[428px] w-full bg-gradient-to-b from-[#0C0C16] to-[#080809] lg:h-[888px]"
>
<ClientOnly>
<swiper-container ref="containerRef" class="size-full mx-auto" loop pagination>
<swiper-container
ref="containerRef"
class="size-full mx-auto"
loop
:pagination="{ clickable: true }"
>
<swiper-slide v-for="(slide, idx) in slides" :key="idx" class="size-full relative">
<div class="size-full relative flex overflow-hidden items-center sm:items-start">
<picture class="absolute w-full h-full left-0 top-0 -z-10">
<source media="(max-width: 480px)" :srcset="slide.bgSm" />
<source media="(max-width: 1280px)" :srcset="slide.bgLg" />
<img :src="slide.bgLg" class="size-full object-cover" />
<!-- <NuxtImg
:src="slide.bgLg"
class="size-full object-cover"
loading="eager"
format="webp"
quality="90"
placeholder
/> -->
</picture>
<div class="w-[1440px] md:w-[870px] sm:w-[300px] sm:top-19 relative mx-auto">
<div class="swiper-banner-info">
@ -138,4 +135,42 @@ const slides = [ @@ -138,4 +135,42 @@ const slides = [
.title-font {
@apply text-5xl md:text-4xl sm:text-lg font-bold text-center text-white;
}
/* Swiper分页器样式 */
:root {
--swiper-pagination-color: #818897;
--swiper-pagination-bullet-width: 48px;
--swiper-pagination-bullet-height: 8px;
--swiper-pagination-bullet-border-radius: 0;
--swiper-pagination-bullet-inactive-color: #818897;
--swiper-pagination-bullet-inactive-opacity: 0.3;
--swiper-pagination-bullet-horizontal-gap: 8px;
}
/* 响应式分页器 - 移动端 */
@media (max-width: 479px) {
:root {
--swiper-pagination-bullet-width: 24px;
--swiper-pagination-bullet-height: 4px;
--swiper-pagination-bullet-horizontal-gap: 4px;
}
}
/* 响应式分页器 - 平板 */
@media (min-width: 480px) and (max-width: 1024px) {
:root {
--swiper-pagination-bullet-width: 36px;
--swiper-pagination-bullet-height: 6px;
--swiper-pagination-bullet-horizontal-gap: 6px;
}
}
/* 响应式分页器 - 大屏 */
@media (min-width: 1025px) {
:root {
--swiper-pagination-bullet-width: 48px;
--swiper-pagination-bullet-height: 8px;
--swiper-pagination-bullet-horizontal-gap: 8px;
}
}
</style>

168
pages/products/BK-1000.vue

@ -29,48 +29,26 @@ @@ -29,48 +29,26 @@
</div>
</div>
</div>
<div
class="h-[888px] max-w-[1440px] md:max-w-[750px] sm:max-w-[320px] w-full relative mx-auto pt-[136px] md:pt-[48px] sm:pt-[24px]"
>
<div v-for="item in slide.content" :key="item.title" class="flex md:flex-col sm:flex-col">
<div class="px-[12%] md:px-0 sm:px-0 md:pt-12 sm:pt-6 md:pb-23 sm:pb-11">
<h3 class="text-5xl sm:text-2xl font-bold text-[#414141] md:text-center sm:text-center">
<div class="max-w-[1200px] md:max-w-[750px] sm:max-w-[320px] w-full relative mx-auto">
<div
v-for="item in slide.content"
:key="item.title"
class="flex border-b py-10 border-[#E5E5E5] gap-10"
>
<div class="px-[12%]">
<h3 class="text-2xl font-bold text-[#414141]">
{{ item.title }}
</h3>
</div>
<div class="flex-1 grid grid-cols-2 md:hidden sm:hidden">
<div v-for="column in item.info" :key="column.column" class="flex flex-col">
<div v-for="info in column.items" :key="info.label" class="min-h-[160px]">
<div class="text-3xl font-bold text-[#17A755] mb-5">{{ info.label }}</div>
<!-- grid grid-cols-2 md:hidden sm:hidden -->
<div class="flex-1 flex items-center">
<div v-for="column in item.info" :key="column.column" class="flex flex-col gap-4">
<div v-for="info in column.items" :key="info.label">
<div class="text-xl font-bold text-[#17A755] mb-1">{{ info.label }}</div>
<p
v-for="value in info.value"
:key="value"
class="text-[18px] text-[#6F6F6F] font-[400]"
>
{{ value }}
</p>
</div>
</div>
</div>
<div class="hidden md:block sm:block">
<div
v-for="column in item.info"
:key="column.column"
class="flex flex-wrap md:gap-5 sm:gap-1"
>
<div
v-for="info in column.items"
:key="info.label"
class="md:min-h-[182px] md:max-w-[640px] sm:max-w-[320px] sm:min-h-[92px] md:min-w-[200px] sm:min-w-[100px]"
:class="{ 'flex-1': column.column === 1 }"
>
<div class="text-3xl sm:text-base font-bold text-[#17A755] mb-5">
{{ info.label }}
</div>
<p
v-for="value in info.value"
:key="value"
class="text-[18px] sm:text-[10px] sm:leading-[10px] text-[#6F6F6F] font-[400]"
class="text-base text-[#6F6F6F] font-[400]"
>
{{ value }}
</p>
@ -84,46 +62,116 @@ @@ -84,46 +62,116 @@
<script setup lang="ts">
const slide = {
bgLg: '/images/banner/bk1000.png',
bgSm: '/images/banner/bk1000_sm.png',
bgLg: '/images/product/BK-1000_lg.png',
bgSm: '/images/product/BK-1000_sm.png',
title: '储能工业控制主机',
subtitle: 'BK-1000',
content: [
{
title: '尺寸与重量',
title: '系统',
info: [
{
column: 1,
items: [
{ label: '长度', value: ['162.6mm'] },
{ label: '宽度', value: ['75.1mm'] },
{ label: '厚度', value: ['8.4mm'] },
{ label: '处理器', value: ['双核Cortex-A7,1.2GHz 工业级'] },
{ label: '内存', value: ['512M'] },
{ label: '板载存储', value: ['4G'] },
{ label: '操作系统', value: ['Linux-Buildroot '] },
],
},
],
},
{
title: '电源',
info: [
{
column: 1,
items: [
{ label: '输入范围', value: ['电压范围DC9-36V'] },
{ label: '防护等级', value: ['防反接,浪涌4KV'] },
{ label: '峰值功耗', value: ['10W'] },
],
},
],
},
{
title: '接口',
info: [
{
column: 1,
items: [
{ label: 'RS485', value: ['6路'] },
{ label: 'CAN', value: ['2路'] },
{ label: 'DO', value: ['4路'] },
{ label: 'DI', value: ['4路'] },
{ label: '以太网', value: ['2路'] },
{ label: '显示接口', value: ['无'] },
{ label: 'USB-A', value: ['2路'] },
{ label: '调试接口', value: ['1路,Type-C'] },
],
},
],
},
{
title: '存储',
info: [
{
column: 2,
column: 1,
items: [
{
label: '重量',
value: [
'约226克(含电池)',
'*实际尺寸与重量依配置,制造工艺、测量方法的不同可能有所差异。',
],
},
{
label: 'xxxx',
value: [
'约226克(含电池)',
'*实际尺寸与重量依配置,制造工艺、测量方法的不同可能有所差异。',
],
},
{ label: 'TF卡', value: ['1路,TF卡'] },
{ label: 'SATA接口', value: ['无'] },
],
},
],
},
{
title: '无线',
info: [
{
column: 1,
items: [
{ label: '4G', value: ['选配'] },
{ label: '北斗/GPS', value: ['选配'] },
{ label: 'wifi', value: ['无'] },
{ label: '蓝牙', value: ['无'] },
],
},
],
},
{
title: '环境',
info: [
{
column: 1,
items: [
{ label: '工作温度', value: ['-40℃~85℃,95%无冷凝'] },
{ label: '存储环境', value: ['-40℃~85℃,95%无冷凝'] },
{ label: '海拔高度', value: ['<3000m'] },
],
},
],
},
{
title: '机械',
info: [
{
column: 1,
items: [
{ label: '尺寸', value: ['244*32*160'] },
{ label: '安装', value: ['壁挂或桌面安装'] },
],
},
],
},
{
title: '防护',
info: [
{
column: 1,
items: [{ label: 'EMC', value: ['电力四级'] }],
},
],
},
],
}
</script>
<style scoped></style>

168
pages/products/BK-2000.vue

@ -29,48 +29,26 @@ @@ -29,48 +29,26 @@
</div>
</div>
</div>
<div
class="h-[888px] max-w-[1440px] md:max-w-[750px] sm:max-w-[320px] w-full relative mx-auto pt-[136px] md:pt-[48px] sm:pt-[24px]"
>
<div v-for="item in slide.content" :key="item.title" class="flex md:flex-col sm:flex-col">
<div class="px-[12%] md:px-0 sm:px-0 md:pt-12 sm:pt-6 md:pb-23 sm:pb-11">
<h3 class="text-5xl sm:text-2xl font-bold text-[#414141] md:text-center sm:text-center">
<div class="max-w-[1200px] md:max-w-[750px] sm:max-w-[320px] w-full relative mx-auto">
<div
v-for="item in slide.content"
:key="item.title"
class="flex border-b py-10 border-[#E5E5E5] gap-10"
>
<div class="px-[12%]">
<h3 class="text-2xl font-bold text-[#414141]">
{{ item.title }}
</h3>
</div>
<div class="flex-1 grid grid-cols-2 md:hidden sm:hidden">
<div v-for="column in item.info" :key="column.column" class="flex flex-col">
<div v-for="info in column.items" :key="info.label" class="min-h-[160px]">
<div class="text-3xl font-bold text-[#17A755] mb-5">{{ info.label }}</div>
<!-- grid grid-cols-2 md:hidden sm:hidden -->
<div class="flex-1 flex items-center">
<div v-for="column in item.info" :key="column.column" class="flex flex-col gap-4">
<div v-for="info in column.items" :key="info.label">
<div class="text-xl font-bold text-[#17A755] mb-1">{{ info.label }}</div>
<p
v-for="value in info.value"
:key="value"
class="text-[18px] text-[#6F6F6F] font-[400]"
>
{{ value }}
</p>
</div>
</div>
</div>
<div class="hidden md:block sm:block">
<div
v-for="column in item.info"
:key="column.column"
class="flex flex-wrap md:gap-5 sm:gap-1"
>
<div
v-for="info in column.items"
:key="info.label"
class="md:min-h-[182px] md:max-w-[640px] sm:max-w-[320px] sm:min-h-[92px] md:min-w-[200px] sm:min-w-[100px]"
:class="{ 'flex-1': column.column === 1 }"
>
<div class="text-3xl sm:text-base font-bold text-[#17A755] mb-5">
{{ info.label }}
</div>
<p
v-for="value in info.value"
:key="value"
class="text-[18px] sm:text-[10px] sm:leading-[10px] text-[#6F6F6F] font-[400]"
class="text-base text-[#6F6F6F] font-[400]"
>
{{ value }}
</p>
@ -84,46 +62,116 @@ @@ -84,46 +62,116 @@
<script setup lang="ts">
const slide = {
bgLg: '/images/banner/bk2000.png',
bgSm: '/images/banner/bk2000_sm.png',
bgLg: '/images/product/BK-2000_lg.png',
bgSm: '/images/product/BK-2000_sm.png',
title: '储能工业控制主机',
subtitle: 'BK-2000',
content: [
{
title: '尺寸与重量',
title: '系统',
info: [
{
column: 1,
items: [
{ label: '长度', value: ['162.6mm'] },
{ label: '宽度', value: ['75.1mm'] },
{ label: '厚度', value: ['8.4mm'] },
{ label: '处理器', value: ['双核Cortex-A7,1.2GHz 工业级'] },
{ label: '内存', value: ['512M'] },
{ label: '板载存储', value: ['4G'] },
{ label: '操作系统', value: ['Linux-Buildroot '] },
],
},
],
},
{
title: '电源',
info: [
{
column: 1,
items: [
{ label: '输入范围', value: ['电压范围DC9-36V'] },
{ label: '防护等级', value: ['防反接,浪涌4KV'] },
{ label: '峰值功耗', value: ['15W'] },
],
},
],
},
{
title: '接口',
info: [
{
column: 1,
items: [
{ label: 'RS485', value: ['8路'] },
{ label: 'CAN', value: ['2路'] },
{ label: 'DO', value: ['10路'] },
{ label: 'DI', value: ['10路'] },
{ label: '以太网', value: ['4路'] },
{ label: '显示接口', value: ['DVI(lvds)'] },
{ label: 'USB-A', value: ['4路'] },
{ label: '调试接口', value: ['1路,Type-C'] },
],
},
],
},
{
title: '存储',
info: [
{
column: 2,
column: 1,
items: [
{
label: '重量',
value: [
'约226克(含电池)',
'*实际尺寸与重量依配置,制造工艺、测量方法的不同可能有所差异。',
],
},
{
label: 'xxxx',
value: [
'约226克(含电池)',
'*实际尺寸与重量依配置,制造工艺、测量方法的不同可能有所差异。',
],
},
{ label: 'TF卡', value: ['1路,TF卡'] },
{ label: 'SATA接口', value: ['无'] },
],
},
],
},
{
title: '无线',
info: [
{
column: 1,
items: [
{ label: '4G', value: ['选配'] },
{ label: '北斗/GPS', value: ['选配'] },
{ label: 'wifi', value: ['无'] },
{ label: '蓝牙', value: ['无'] },
],
},
],
},
{
title: '环境',
info: [
{
column: 1,
items: [
{ label: '工作温度', value: ['-40℃~85℃,95%无冷凝'] },
{ label: '存储环境', value: ['-40℃~85℃,95%无冷凝'] },
{ label: '海拔高度', value: ['<3000m'] },
],
},
],
},
{
title: '机械',
info: [
{
column: 1,
items: [
{ label: '尺寸', value: ['244*45*160'] },
{ label: '安装', value: ['壁挂或桌面安装'] },
],
},
],
},
{
title: '防护',
info: [
{
column: 1,
items: [{ label: 'EMC', value: ['电力四级'] }],
},
],
},
],
}
</script>
<style scoped></style>

85
pages/text.vue

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
<template>
<ul
class="grid grid-cols-1 grid-rows-none gap-4 overflow-auto xl:max-h-[56rem] xl:grid-rows-2 lg:gap-4 md:grid-cols-12 md:grid-rows-3"
>
<li
v-for="item in gridItems"
:key="item.title"
:class="cn('min-h-[14rem] list-none', item.area)"
>
<div class="rounded-2.5xl relative h-full border p-2 md:rounded-3xl md:p-3">
<GlowingEffect
:spread="40"
:glow="true"
:disabled="false"
:proximity="64"
:inactive-zone="0.01"
/>
<div
class="border-0.75 relative flex h-full flex-col justify-between gap-6 overflow-hidden rounded-xl p-6 md:p-6 dark:shadow-[0px_0px_27px_0px_#2D2D2D]"
>
<div class="relative flex flex-1 flex-col justify-between gap-3">
<div class="w-fit rounded-lg border border-gray-600 p-2">
<Icon class="size-4 text-black dark:text-neutral-500" :name="item.icon"></Icon>
</div>
<div class="space-y-3">
<h3
class="-tracking-4 text-balance pt-0.5 font-sans text-xl/[1.375rem] font-semibold text-black md:text-2xl/[1.875rem] dark:text-white"
>
{{ item.title }}
</h3>
<h2
class="font-sans text-sm/[1.125rem] text-black md:text-base/[1.375rem] dark:text-neutral-400 [&_b]:md:font-semibold [&_strong]:md:font-semibold"
>
{{ item.description }}
</h2>
</div>
</div>
</div>
</div>
</li>
</ul>
</template>
<script lang="ts" setup>
import GlowingEffect from '@/components/ui/glowing-effect/GlowingEffect.vue'
import { cn } from '@/lib/utils'
const gridItems = [
{
area: 'md:[grid-area:1/1/2/7] xl:[grid-area:1/1/2/5]',
icon: 'lucide:box',
title: 'Unbox Endless Possibilities',
description:
'Open up Inspira UI to discover so many features, you’ll wonder if you’ve wandered into a magical subscription for infinite goodies.',
},
{
area: 'md:[grid-area:1/7/2/13] xl:[grid-area:2/1/3/5]',
icon: 'lucide:settings',
title: 'Crank the Dials to Eleven',
description:
'We packed Inspira UI with enough customizable settings to keep you tweaking forever. If it’s broken, you probably forgot to flip one more switch!',
},
{
area: 'md:[grid-area:2/1/3/7] lg:[grid-area:1/5/3/8]',
icon: 'lucide:music',
title: 'Dance Your Way to Better UI',
description:
'Forget dull interfaces—Inspira UI brings your Vue and Nuxt apps to life with animations so smooth, you’ll wonder!',
},
{
area: 'md:[grid-area:2/7/3/13] xl:[grid-area:1/8/2/13]',
icon: 'lucide:sparkles',
title: 'Spark a Little Magic',
description:
'Make your interface shine brighter than your future. Inspira UI turns that dull design into an enchanting experience—fairy dust included!',
},
{
area: 'md:[grid-area:3/1/4/13] xl:[grid-area:2/8/3/13]',
icon: 'lucide:search',
title: 'Seek and You Shall Find',
description:
'Our search is so advanced it might unearth your lost socks. Just don’t blame us when you realize they don’t match!',
},
]
</script>

924
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

BIN
public/images/aboutUs/bg.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 856 KiB

BIN
public/images/banner/bk1000.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 862 KiB

BIN
public/images/banner/bk1000_1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 KiB

BIN
public/images/banner/bk1000_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
public/images/banner/bk2000.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 916 KiB

BIN
public/images/banner/bk2000_1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

BIN
public/images/banner/bk2000_2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
public/images/product/BK-1000_lg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

BIN
public/images/product/BK-1000_sm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
public/images/product/BK-2000_lg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

BIN
public/images/product/Bk-2000_sm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Loading…
Cancel
Save