首页布局
All checks were successful
site-dist-deploy / build-and-deploy (push) Successful in 1m19s

This commit is contained in:
speakeloudest 2026-02-02 09:58:51 +02:00
parent 084bec5f67
commit b75bac345c
22 changed files with 366 additions and 30 deletions

50
package-lock.json generated
View File

@ -10,11 +10,12 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.2.0",
"axios": "^1.13.2", "axios": "^1.13.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"embla-carousel-vue": "^8.6.0",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"reka-ui": "^2.8.0", "reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
@ -2370,14 +2371,14 @@
} }
}, },
"node_modules/@vueuse/core": { "node_modules/@vueuse/core": {
"version": "14.1.0", "version": "14.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.0.tgz",
"integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", "integrity": "sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/web-bluetooth": "^0.0.21", "@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.1.0", "@vueuse/metadata": "14.2.0",
"@vueuse/shared": "14.1.0" "@vueuse/shared": "14.2.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
@ -2387,14 +2388,18 @@
} }
}, },
"node_modules/@vueuse/metadata": { "node_modules/@vueuse/metadata": {
"version": "14.1.0", "version": "14.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.0.tgz",
"integrity": "sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/@vueuse/shared": { "node_modules/@vueuse/shared": {
"version": "14.1.0", "version": "14.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.0.tgz",
"integrity": "sha512-Z0bmluZTlAXgUcJ4uAFaML16JcD8V0QG00Db3quR642I99JXIDRa2MI2LGxiLVhcBjVnL1jOzIvT5TT2lqJlkA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
@ -2966,6 +2971,35 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT",
"peer": true
},
"node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/embla-carousel-vue": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-vue/-/embla-carousel-vue-8.6.0.tgz",
"integrity": "sha512-v8UO5UsyLocZnu/LbfQA7Dn2QHuZKurJY93VUmZYP//QRWoCWOsionmvLLAlibkET3pGPs7++03VhJKbWD7vhQ==",
"license": "MIT",
"dependencies": {
"embla-carousel": "8.6.0",
"embla-carousel-reactive-utils": "8.6.0"
},
"peerDependencies": {
"vue": "^3.2.37"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.4", "version": "5.18.4",
"license": "MIT", "license": "MIT",

View File

@ -18,11 +18,12 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.2.0",
"axios": "^1.13.2", "axios": "^1.13.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"embla-carousel-vue": "^8.6.0",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"reka-ui": "^2.8.0", "reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",

BIN
public/fast-video.MP4 Normal file

Binary file not shown.

View File

@ -1,34 +1,37 @@
import type { VariantProps } from 'class-variance-authority' import type { VariantProps } from "class-variance-authority"
import { cva } from 'class-variance-authority' import { cva } from "class-variance-authority"
export { default as Button } from './Button.vue' export { default as Button } from "./Button.vue"
export const buttonVariants = cva( export const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90', default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', secondary:
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', "bg-secondary text-secondary-foreground hover:bg-secondary/80",
link: 'text-primary underline-offset-4 hover:underline', ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3', "default": "h-9 px-4 py-2 has-[>svg]:px-3",
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', "lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: 'size-9', "icon": "size-9",
'icon-sm': 'size-8', "icon-sm": "size-8",
'icon-lg': 'size-10', "icon-lg": "size-10",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default', size: "default",
}, },
}, },
) )

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import type { CarouselEmits, CarouselProps, WithClassAsProps } from "./interface"
import { cn } from "@/lib/utils"
import { useProvideCarousel } from "./useCarousel"
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: "horizontal",
})
const emits = defineEmits<CarouselEmits>()
const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } = useProvideCarousel(props, emits)
defineExpose({
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev,
})
function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === "vertical" ? "ArrowUp" : "ArrowLeft"
const nextKey = props.orientation === "vertical" ? "ArrowDown" : "ArrowRight"
if (event.key === prevKey) {
event.preventDefault()
scrollPrev()
return
}
if (event.key === nextKey) {
event.preventDefault()
scrollNext()
}
}
</script>
<template>
<div
data-slot="carousel"
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown"
>
<slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
</div>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { WithClassAsProps } from "./interface"
import { cn } from "@/lib/utils"
import { useCarousel } from "./useCarousel"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<WithClassAsProps>()
const { carouselRef, orientation } = useCarousel()
</script>
<template>
<div
ref="carouselRef"
data-slot="carousel-content"
class="overflow-hidden"
>
<div
:class="
cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
props.class,
)"
v-bind="$attrs"
>
<slot />
</div>
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { WithClassAsProps } from "./interface"
import { cn } from "@/lib/utils"
import { useCarousel } from "./useCarousel"
const props = defineProps<WithClassAsProps>()
const { orientation } = useCarousel()
</script>
<template>
<div
data-slot="carousel-item"
role="group"
aria-roledescription="slide"
:class="cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
props.class,
)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { WithClassAsProps } from "./interface"
import { cn } from "@/lib/utils"
import { useCarousel } from "./useCarousel"
import ArrowIcon from '@/pages/Home/modules4/arrow.svg?component'
const props = defineProps<WithClassAsProps>()
const { orientation, canScrollNext, scrollNext } = useCarousel()
</script>
<template>
<button
data-slot="carousel-next"
:class="cn(
'absolute cursor-pointer z-10',
orientation === 'horizontal'
? 'top-1/2 -right-12 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
@click="scrollNext"
>
<slot>
<ArrowIcon class="h-[120px] w-auto rotate-180" />
<span class="sr-only">Next Slide</span>
</slot>
</button>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { WithClassAsProps } from "./interface"
import { cn } from "@/lib/utils"
import { useCarousel } from "./useCarousel"
import ArrowIcon from '@/pages/Home/modules4/arrow.svg?component'
const props = defineProps<WithClassAsProps>()
const { orientation, canScrollPrev, scrollPrev } = useCarousel()
</script>
<template>
<button
data-slot="carousel-previous"
:class="cn(
'absolute cursor-pointer z-10',
orientation === 'horizontal'
? 'top-1/2 -left-12 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
@click="scrollPrev"
>
<slot>
<ArrowIcon class="h-[120px] w-auto" />
<span class="sr-only">Previous Slide</span>
</slot>
</button>
</template>

View File

@ -0,0 +1,10 @@
export { default as Carousel } from "./Carousel.vue"
export { default as CarouselContent } from "./CarouselContent.vue"
export { default as CarouselItem } from "./CarouselItem.vue"
export { default as CarouselNext } from "./CarouselNext.vue"
export { default as CarouselPrevious } from "./CarouselPrevious.vue"
export type {
UnwrapRefCarouselApi as CarouselApi,
} from "./interface"
export { useCarousel } from "./useCarousel"

View File

@ -0,0 +1,26 @@
import type useEmblaCarousel from "embla-carousel-vue"
import type {
EmblaCarouselVueType,
} from "embla-carousel-vue"
import type { HTMLAttributes, UnwrapRef } from "vue"
type CarouselApi = EmblaCarouselVueType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
export type UnwrapRefCarouselApi = UnwrapRef<CarouselApi>
export interface CarouselProps {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
}
export interface CarouselEmits {
(e: "init-api", payload: UnwrapRefCarouselApi): void
}
export interface WithClassAsProps {
class?: HTMLAttributes["class"]
}

View File

@ -0,0 +1,56 @@
import type { UnwrapRefCarouselApi as CarouselApi, CarouselEmits, CarouselProps } from "./interface"
import { createInjectionState } from "@vueuse/core"
import emblaCarouselVue from "embla-carousel-vue"
import { onMounted, ref } from "vue"
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
({
opts,
orientation,
plugins,
}: CarouselProps, emits: CarouselEmits) => {
const [emblaNode, emblaApi] = emblaCarouselVue({
...opts,
axis: orientation === "horizontal" ? "x" : "y",
}, plugins)
function scrollPrev() {
emblaApi.value?.scrollPrev()
}
function scrollNext() {
emblaApi.value?.scrollNext()
}
const canScrollNext = ref(false)
const canScrollPrev = ref(false)
function onSelect(api: CarouselApi) {
canScrollNext.value = api?.canScrollNext() || false
canScrollPrev.value = api?.canScrollPrev() || false
}
onMounted(() => {
if (!emblaApi.value)
return
emblaApi.value?.on("init", onSelect)
emblaApi.value?.on("reInit", onSelect)
emblaApi.value?.on("select", onSelect)
emits("init-api", emblaApi.value)
})
return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
},
)
function useCarousel() {
const carouselState = useInjectCarousel()
if (!carouselState)
throw new Error("useCarousel must be used within a <Carousel />")
return carouselState
}
export { useCarousel, useProvideCarousel }

View File

@ -37,8 +37,8 @@
<!-- Main Content Container --> <!-- Main Content Container -->
<div class="container mx-auto flex flex-col"> <div class="container mx-auto flex flex-col">
<main class="pt-10"> <main class="pt-10">
<!-- module1 --> <!-- module0 -->
<div class="mb-[80px] flex"> <div class="mb-[80px] flex justify-between">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="mb-[20px] ml-[42px] md:ml-[17px]"> <div class="mb-[20px] ml-[42px] md:ml-[17px]">
<h2 class="mb-2 text-2xl font-black md:text-8xl"> <h2 class="mb-2 text-2xl font-black md:text-8xl">
@ -49,6 +49,11 @@
<img src="./Group%20133.png" alt="" class="mt-[52px] h-[287px] w-[182px]" /> <img src="./Group%20133.png" alt="" class="mt-[52px] h-[287px] w-[182px]" />
</div> </div>
<!-- video --> <!-- video -->
<div class="h-[450px] w-[800px] bg-[url('@/pages/Home/modules1/Group-236.png')]"></div>
</div>
<!-- modules1 -->
<div class="mb-[80px] w-full">
<img src="./modules1/Group%20235.png" alt="邀请规则" class="mx-auto w-[1024px]" />
</div> </div>
<!-- modules2 --> <!-- modules2 -->
<div class="mb-[80px] w-full rounded-[40px] bg-[#ADFF5B] p-8 pb-2 text-black"> <div class="mb-[80px] w-full rounded-[40px] bg-[#ADFF5B] p-8 pb-2 text-black">
@ -71,7 +76,22 @@
</div> </div>
<div class="text-center text-[#999999]">给您的客户提供专业视角的建议</div> <div class="text-center text-[#999999]">给您的客户提供专业视角的建议</div>
<div class="mt-[20px] flex justify-center"> <div class="mt-[20px] flex justify-center">
<div class="size-[600px] rounded-[40px] border-8 border-[#ADFF5B]"></div> <Carousel class="relative w-full max-w-[700px]" :opts="{ loop: true }">
<div class="size-[700px] overflow-hidden rounded-[40px]">
<CarouselContent>
<CarouselItem v-for="(img, index) in modules4Images" :key="index">
<img
:src="img"
alt=""
class="h-[700px] w-[700px] object-cover select-none"
draggable="false"
/>
</CarouselItem>
</CarouselContent>
</div>
<CarouselPrevious class="-left-[100px]" />
<CarouselNext class="-right-[100px]" />
</Carousel>
</div> </div>
</div> </div>
</main> </main>
@ -97,6 +117,13 @@ import { useLocalStorage } from '@vueuse/core'
import LoginFormModal from './components/LoginFormModal.vue' import LoginFormModal from './components/LoginFormModal.vue'
import Logo from './logo.svg?component' import Logo from './logo.svg?component'
import Group215Icon from './modules3/Group 215.svg?component' import Group215Icon from './modules3/Group 215.svg?component'
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/components/ui/carousel'
import request from '@/utils/request' import request from '@/utils/request'
import Frame98 from './modules2/Frame 98.png' import Frame98 from './modules2/Frame 98.png'
import Frame99 from './modules2/Frame 99.png' import Frame99 from './modules2/Frame 99.png'
@ -105,7 +132,15 @@ import Frame101 from './modules2/Frame 101.png'
import Frame104 from './modules2/Frame 104.png' import Frame104 from './modules2/Frame 104.png'
import Frame105 from './modules2/Frame 105.png' import Frame105 from './modules2/Frame 105.png'
import image1 from './modules4/image1.png'
import image2 from './modules4/image2.png'
import image3 from './modules4/image3.png'
import image4 from './modules4/image4.png'
import image5 from './modules4/image5.png'
import image6 from './modules4/image6.png'
const modules2Image = [Frame98, Frame99, Frame100, Frame101, Frame104, Frame105] const modules2Image = [Frame98, Frame99, Frame100, Frame101, Frame104, Frame105]
const modules4Images = [image1, image2, image3, image4, image5, image6]
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,3 @@
<svg width="48" height="120" viewBox="0 0 48 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 59.9033L37.5049 0L47.4883 4.35156L12.8008 59.9033L47.4883 115.456L37.5049 119.808L0 59.9033Z" fill="#ADFF5B"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB