Browse Source

feat: 项目结构

main
betaqi 4 months ago
commit
740b95b79a
  1. 24
      .gitignore
  2. 10
      .prettierrc
  3. 2
      .vscode/settings.json
  4. 75
      README.md
  5. 5
      app.vue
  6. BIN
      assets/images/bg.jpg
  7. 83
      assets/styles/base.css
  8. 9
      assets/styles/main.css
  9. 20
      components.json
  10. 139
      components/LampEffect.vue
  11. 253
      components/Vortex.vue
  12. 26
      components/ui/button/Button.vue
  13. 35
      components/ui/button/index.ts
  14. 238
      layouts/default.vue
  15. 15
      lib/utils.ts
  16. 52
      nuxt.config.ts
  17. 39
      package.json
  18. 9
      pages/[...slug].vue
  19. 12
      pages/aboutUs.vue
  20. 11
      pages/index.vue
  21. 12
      pages/product.vue
  22. 7303
      pnpm-lock.yaml
  23. BIN
      public/favicon.ico
  24. 1
      public/robots.txt
  25. 3
      server/tsconfig.json
  26. 94
      tailwind.config.js
  27. 4
      tsconfig.json

24
.gitignore vendored

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

10
.prettierrc

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./assets/styles/main.css",
"tailwindConfig": "./tailwind.config.js",
"tailwindFunctions": ["clsx"]
}

2
.vscode/settings.json vendored

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
{
}

75
README.md

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

5
app.vue

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

BIN
assets/images/bg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

83
assets/styles/base.css

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

9
assets/styles/main.css

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
@import './base.css';
html {
@apply w-full h-full;
}
body {
@apply w-full h-full;
}

20
components.json

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/styles/base.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
}

139
components/LampEffect.vue

@ -0,0 +1,139 @@ @@ -0,0 +1,139 @@
<template>
<div
:class="
cn(
'relative z-0 flex min-h-screen w-full flex-col items-center justify-center overflow-hidden rounded-md bg-slate-950',
$props.class,
)
"
>
<div class="relative isolate z-0 flex w-full flex-1 scale-y-125 items-center justify-center">
<!-- Conic Gradient -->
<div
:style="{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}"
class="animate-conic-gradient bg-gradient-conic absolute inset-auto right-1/2 h-56 w-60 overflow-visible from-cyan-500 via-transparent to-transparent text-white opacity-50 [--conic-position:from_70deg_at_center_top]"
>
<div
class="absolute bottom-0 left-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]"
/>
<div
class="absolute bottom-0 left-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_right,white,transparent)]"
/>
</div>
<div
:style="{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}"
class="animate-conic-gradient bg-gradient-conic absolute inset-auto left-1/2 h-56 w-60 from-transparent via-transparent to-cyan-500 text-white opacity-50 [--conic-position:from_290deg_at_center_top]"
>
<div
class="absolute bottom-0 right-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_left,white,transparent)]"
/>
<div
class="absolute bottom-0 right-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]"
/>
</div>
<div
class="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"
></div>
<div
class="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"
></div>
<div
class="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"
></div>
<!-- Spotlight -->
<div
class="animate-spotlight absolute inset-auto z-30 h-36 w-32 -translate-y-24 rounded-full bg-cyan-400 blur-2xl"
></div>
<!-- Glowing Line -->
<div
class="animate-glowing-line absolute inset-auto z-50 h-0.5 w-60 -translate-y-28 bg-cyan-400"
></div>
<div class="absolute inset-auto z-40 h-44 w-full translate-y-[-12.5rem] bg-slate-950"></div>
</div>
<div class="relative z-50 flex -translate-y-80 flex-col items-center px-5">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
interface LampEffectProps {
delay?: number
duration?: number
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<LampEffectProps>(), {
delay: 0.5,
duration: 0.8,
})
const durationInSeconds = computed(() => `${props.duration}s`)
const delayInSeconds = computed(() => `${props.delay}s`)
</script>
<style scoped>
/* Spotlight Animation */
.animate-spotlight {
animation: spotlight-anim ease-in-out v-bind(durationInSeconds) forwards;
animation-delay: v-bind(delayInSeconds);
}
/* Glowing Line Animation */
.animate-glowing-line {
animation: glowing-line-anim ease-in-out v-bind(durationInSeconds) forwards;
animation-delay: v-bind(delayInSeconds);
}
/* Conic Gradient Animation */
.animate-conic-gradient {
animation: conic-gradient-anim ease-in-out v-bind(durationInSeconds) forwards;
animation-delay: v-bind(delayInSeconds);
}
/* Keyframes for Spotlight */
@keyframes spotlight-anim {
from {
width: 8rem;
}
to {
width: 16rem;
}
}
/* Keyframes for Glowing Line */
@keyframes glowing-line-anim {
from {
width: 15rem;
}
to {
width: 30rem;
}
}
/* Keyframes for Conic Gradient */
@keyframes conic-gradient-anim {
from {
opacity: 0.5;
width: 15rem;
}
to {
opacity: 1;
width: 30rem;
}
}
</style>

253
components/Vortex.vue

@ -0,0 +1,253 @@ @@ -0,0 +1,253 @@
<template>
<div :class="cn('relative h-full w-full', props.containerClass)">
<Motion
ref="containerRef"
as="div"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent"
>
<canvas ref="canvasRef"></canvas>
</Motion>
<div :class="cn('relative z-10', props.class)">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
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
const PARTICLE_PROP_COUNT = 9
const RANGE_HUE = 100
const NOISE_STEPS = 3
const X_OFF = 0.00125
const Y_OFF = 0.00125
const Z_OFF = 0.0005
interface VortexProps {
class?: string
containerClass?: string
particleCount?: number
rangeY?: number
baseHue?: number
baseSpeed?: number
rangeSpeed?: number
baseRadius?: number
rangeRadius?: number
backgroundColor?: string
}
const props = withDefaults(defineProps<VortexProps>(), {
particleCount: 700,
rangeY: 100,
baseSpeed: 0.0,
rangeSpeed: 1.5,
baseRadius: 1,
rangeRadius: 2,
baseHue: 220,
backgroundColor: '#000000',
})
const tick = ref<number>(0)
const animationFrame = ref<number | null>(null)
const particleProps = shallowRef<Float32Array | null>(null)
const center = ref<[number, number]>([0, 0])
const ctx = shallowRef<CanvasRenderingContext2D | null>(null)
const canvasRef = templateRef<HTMLCanvasElement | null>('canvasRef')
const containerRef = templateRef<HTMLElement | null>('containerRef')
const particleCache = {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
ttl: 0,
speed: 0,
radius: 0,
hue: 0,
}
const noise3D = createNoise3D()
function rand(n: number) {
return n * Math.random()
}
function randRange(n: number): number {
return n - rand(2 * n)
}
function fadeInOut(t: number, m: number): number {
const hm = 0.5 * m
return Math.abs(((t + hm) % m) - hm) / hm
}
function lerp(n1: number, n2: number, speed: number): number {
return (1 - speed) * n1 + speed * n2
}
function initParticle(i: number) {
if (!particleProps.value || !canvasRef.value) return
const canvas = canvasRef.value
particleCache.x = rand(canvas.width)
particleCache.y = center.value[1] + randRange(props.rangeY)
particleCache.vx = 0
particleCache.vy = 0
particleCache.life = 0
particleCache.ttl = BASE_TTL + rand(RANGE_TTL)
particleCache.speed = props.baseSpeed + rand(props.rangeSpeed)
particleCache.radius = props.baseRadius + rand(props.rangeRadius)
particleCache.hue = props.baseHue + rand(RANGE_HUE)
particleProps.value.set(
[
particleCache.x,
particleCache.y,
particleCache.vx,
particleCache.vy,
particleCache.life,
particleCache.ttl,
particleCache.speed,
particleCache.radius,
particleCache.hue,
],
i,
)
}
function updateParticle(i: number) {
if (!particleProps.value || !canvasRef.value || !ctx.value) return
const canvas = canvasRef.value
const props = particleProps.value
const context = ctx.value
particleCache.x = props[i]
particleCache.y = props[i + 1]
particleCache.vx = props[i + 2]
particleCache.vy = props[i + 3]
particleCache.life = props[i + 4]
particleCache.ttl = props[i + 5]
particleCache.speed = props[i + 6]
particleCache.radius = props[i + 7]
particleCache.hue = props[i + 8]
const n =
noise3D(particleCache.x * X_OFF, particleCache.y * Y_OFF, tick.value * Z_OFF) *
NOISE_STEPS *
TAU
const nextVx = lerp(particleCache.vx, Math.cos(n), 0.5)
const nextVy = lerp(particleCache.vy, Math.sin(n), 0.5)
const nextX = particleCache.x + nextVx * particleCache.speed
const nextY = particleCache.y + nextVy * particleCache.speed
context.save()
context.lineCap = 'round'
context.lineWidth = particleCache.radius
context.strokeStyle = `hsla(${particleCache.hue},100%,60%,${fadeInOut(
particleCache.life,
particleCache.ttl,
)})`
context.beginPath()
context.moveTo(particleCache.x, particleCache.y)
context.lineTo(nextX, nextY)
context.stroke()
context.restore()
props[i] = nextX
props[i + 1] = nextY
props[i + 2] = nextVx
props[i + 3] = nextVy
props[i + 4] = particleCache.life + 1
if (
nextX > canvas.width ||
nextX < 0 ||
nextY > canvas.height ||
nextY < 0 ||
particleCache.life > particleCache.ttl
) {
initParticle(i)
}
}
function draw() {
if (!canvasRef.value || !ctx.value || !particleProps.value) return
const canvas = canvasRef.value
const context = ctx.value
tick.value++
context.fillStyle = props.backgroundColor
context.fillRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < particleProps.value.length; i += PARTICLE_PROP_COUNT) {
updateParticle(i)
}
context.save()
context.filter = 'blur(8px) brightness(200%)'
context.globalCompositeOperation = 'lighter'
context.drawImage(canvas, 0, 0)
context.restore()
context.save()
context.filter = 'blur(4px) brightness(200%)'
context.globalCompositeOperation = 'lighter'
context.drawImage(canvas, 0, 0)
context.restore()
animationFrame.value = requestAnimationFrame(draw)
}
const handleResize = useDebounceFn(() => {
if (!canvasRef.value) return
const canvas = canvasRef.value
const { innerWidth, innerHeight } = window
canvas.width = innerWidth
canvas.height = innerHeight
center.value = [0.5 * canvas.width, 0.5 * canvas.height]
}, 150)
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
ctx.value = canvas.getContext('2d')
if (!ctx.value) return
canvas.width = window.innerWidth
canvas.height = window.innerHeight
center.value = [0.5 * canvas.width, 0.5 * canvas.height]
const particlePropsLength = props.particleCount * PARTICLE_PROP_COUNT
particleProps.value = new Float32Array(particlePropsLength)
for (let i = 0; i < particlePropsLength; i += PARTICLE_PROP_COUNT) {
initParticle(i)
}
draw()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
}
window.removeEventListener('resize', handleResize)
ctx.value = null
particleProps.value = null
})
</script>

26
components/ui/button/Button.vue

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

35
components/ui/button/index.ts

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground active:bg-accent/900',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

238
layouts/default.vue

@ -0,0 +1,238 @@ @@ -0,0 +1,238 @@
<template>
<div>
<header
class="z-1000 fixed left-0 right-0 top-[-1] w-full bg-transparent duration-300"
:class="{ 'backdrop-blur-2xl': isScrolled || isNavbarOpen, 'h-full': isNavbarOpen }"
>
<div class="header-bg flex h-13 w-full items-center justify-between px-6 sm:px-4 md:px-8">
<h1 class="text-2xl font-bold text-[#52AC63]">BTDK</h1>
<nav class="absolute left-0 right-0 top-0">
<ul class="flex justify-center gap-8 leading-13 text-white sm:hidden md:hidden">
<li
v-for="nav in navItems"
:key="nav.path"
class="relative h-full text-inherit after:absolute after:bottom-2 after:left-0 after:right-0 after:h-[1px] after:w-full after:origin-top-right after:scale-x-0 after:bg-[#52AC63] after:transition-[transform] after:duration-300 after:content-[''] hover:after:origin-top-left hover:after:scale-x-100"
>
<NuxtLink :to="nav.path" class="text-inherit">
{{ nav.name }}
</NuxtLink>
</li>
</ul>
</nav>
<Menu as="div" class="hidden h-full sm:block md:block" v-slot="{ open: menuOpen }">
<MenuButton class="h-full">
<div
class="navbar-control"
:class="{ open: isNavbarOpen }"
@click="toggleNavbar(menuOpen)"
>
<span class="control-icon"></span>
<span class="control-icon"></span>
<span class="control-icon"></span>
</div>
</MenuButton>
<transition
enter-active-class="transition-all duration-300 overflow-hidden"
enter-from-class="max-h-0 opacity-70"
enter-to-class="h-full opacity-100"
leave-active-class="transition-all duration-200 overflow-hidden"
leave-from-class="h-full opacity-100"
leave-to-class="max-h-0 opacity-70"
>
<!-- lg:ml-[-110px] flex justify-center md:justify-start md:flex-col md:text-center md:overflow-auto md:grow-[1] -->
<div v-show="isNavbarOpen">
<MenuItems
as="ul"
class="header-bg z-1000 border-t-1 absolute inset-0 top-13 overflow-hidden border-t-[1px] border-[hsla(0,0%,100%,.06)] text-white backdrop-blur-2xl"
static
>
<template v-for="nav in navItems" :key="nav.path">
<MenuItem
as="li"
v-slot="{ close }"
class="cursor-pointer text-nowrap text-center"
@click="onNavClick(nav, menuOpen)"
>
<Disclosure as="div" class="size-full" v-slot="{ open }">
<DisclosureButton as="p" class="relative py-7">
<span>{{ nav.name }} </span>
<Icon
name="mingcute:up-line"
:class="open ? '' : 'rotate-180 transform'"
class="absolute right-5 top-1/2 -translate-y-1/2 text-lg transition-transform duration-300"
:ssr="true"
v-if="!!nav.children?.length"
/>
</DisclosureButton>
<transition
enter-active-class="transition-all duration-700 overflow-hidden"
enter-from-class="max-h-0 opacity-70"
enter-to-class="max-h-[300px] opacity-100"
leave-active-class="transition-all duration-200 overflow-hidden"
leave-from-class="max-h-[300px] opacity-100"
leave-to-class="max-h-0 opacity-70"
>
<DisclosurePanel as="div" v-if="!!nav.children?.length">
<ul class="flex flex-col bg-[hsla(0,0%,100%,.06)]">
<li
class="text-nowrap py-7 text-center"
v-for="child in nav.children"
:key="child.path"
@click="onNavClick(child, menuOpen)"
>
<span>
{{ child.name }}
</span>
</li>
</ul>
</DisclosurePanel>
</transition>
</Disclosure>
</MenuItem>
</template>
</MenuItems>
</div>
</transition>
</Menu>
</div>
</header>
<slot />
<footer>
<p>版权所有 © 2025 比特电科</p>
</footer>
</div>
</template>
<script setup lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
const isScrolled = ref(false)
const router = useRouter()
const checkScroll = () => {
isScrolled.value = window.scrollY > 20
}
onMounted(() => {
if (typeof window !== 'undefined') {
checkScroll()
window.addEventListener('scroll', checkScroll)
}
})
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('scroll', checkScroll)
}
})
const isNavbarOpen = ref(false)
const toggleNavbar = (open: boolean) => {
isNavbarOpen.value = !open
}
function onNavClick(nav: any, menuOpen: boolean) {
if (!!nav.children?.length) {
return
}
router.push(nav.path)
isNavbarOpen.value = false
}
const navItems = [
{
name: '首页',
path: '/',
},
{
name: '产品中心',
path: '/products',
children: [
{
name: 'EM系列储能边缘智能网关',
path: '/products/em-series-energy-storage-edge-intelligent-gateway',
},
{
name: 'EM系列储能边缘智能网关',
path: '/products/em-series-energy-storage-edge-intelligent-gateway',
},
],
},
{
name: '技术资料',
path: '/technical-materials',
},
{
name: '在线商城',
path: '/online-store',
},
{
name: '关于我们',
path: '/about',
},
]
</script>
<style scoped>
.header-bg {
@apply bg-[rgba(92,92,92,0.4)] bg-gradient-to-r from-black/40 to-black/40;
}
/* 子菜单展开收缩动画 */
.transition-all {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* CSS */
.navbar-control {
cursor: pointer;
height: 100%;
display: flex;
flex-direction: column;
gap: 6px;
justify-content: center;
}
.control-icon {
transition: all 0.3s ease;
}
/* 每个 control-icon 的伪元素 */
.control-icon:before {
background-color: #fff;
border-bottom-left-radius: 1px;
border-top-right-radius: 1px;
content: '';
display: block;
height: 1px;
transition:
transform 0.3s ease 0.2s,
background-color 0.4s ease 0s;
width: 22px;
opacity: 0.9;
}
.navbar-control.open .control-icon:before {
/* background-color: #000; */
}
.navbar-control.open .control-icon:nth-child(1) {
transform: translateY(7px);
}
.navbar-control.open .control-icon:nth-child(2) {
opacity: 0;
transition-duration: 0.3s;
}
.navbar-control .control-icon:nth-child(2) {
transition-duration: 0.6s;
}
.navbar-control.open .control-icon:nth-child(3) {
transform: translateY(-7px);
}
/* open 状态下的动画 */
.navbar-control.open .control-icon:nth-child(1):before {
transform: rotate(45deg);
}
.navbar-control.open .control-icon:nth-child(3):before {
transform: rotate(-45deg);
}
</style>

15
lib/utils.ts

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
import type { Updater } from '@tanstack/vue-table'
import type { Ref } from 'vue'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
ref.value
= typeof updaterOrValue === 'function'
? updaterOrValue(ref.value)
: updaterOrValue
}

52
nuxt.config.ts

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
modules: ['motion-v/nuxt', '@nuxtjs/tailwindcss', 'shadcn-nuxt', '@nuxt/icon'],
devtools: { enabled: true },
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
css: ['~/assets/styles/main.css'],
vite: {
plugins: [
],
},
app: {
head: {
title: 'EM系列储能边缘智能网关-比特电科',
meta: [
{
name: 'description',
content: 'EM系列储能边缘智能网关-比特电科',
},
{
name: 'keywords',
content: 'EM系列储能边缘智能网关-比特电科',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1.0',
},
],
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
noscript: [
{
innerHTML: 'JavaScript is required',
},
],
},
},
devServer: {
port: 3300, // 设置开发服务器端口
host: '0.0.0.0', // 可选:允许外部访问
},
})

39
package.json

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@vueuse/core": "^13.0.0",
"class-variance-authority": "^0.7.1",
"lucide-vue-next": "^0.486.0",
"motion-v": "1.0.0-alpha.1",
"nuxt": "^3.16.1",
"reka-ui": "^2.1.1",
"shadcn-nuxt": "1.0.3",
"simplex-noise": "^4.0.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@iconify-json/mingcute": "^1.2.3",
"@inspira-ui/plugins": "^0.0.1",
"@nuxt/icon": "^1.11.0",
"@nuxtjs/tailwindcss": "^6.13.2",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwind-merge": "^3.1.0",
"tailwindcss-animate": "^1.0.7"
},
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
}

9
pages/[...slug].vue

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
<template>
<div>
<h1>404</h1>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

12
pages/aboutUs.vue

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
<template>
<div></div>
</template>
<script setup lang="ts">
useSeoMeta({
title: '关于我们-比特电科',
description: '关于我们-比特电科',
})
</script>
<style scoped></style>

11
pages/index.vue

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
<template>
<main class="h-screen w-full">
<img src="../assets/images/bg.jpg" class="h-[32rem] w-full object-cover" />
</main>
</template>
<script setup lang="ts">
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
</script>
<style scoped></style>

12
pages/product.vue

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
<template>
<div></div>
</template>
<script setup lang="ts">
useSeoMeta({
title: '产品中心-比特电科',
description: '产品中心-比特电科',
})
</script>
<style scoped></style>

7303
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/robots.txt

@ -0,0 +1 @@ @@ -0,0 +1 @@

3
server/tsconfig.json

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

94
tailwind.config.js

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
import animate from 'tailwindcss-animate'
import { setupInspiraUI } from '@inspira-ui/plugins'
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['selector', 'class'],
safelist: ['dark'],
content: [
'./components/**/*.{js,vue,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./app.vue',
'./error.vue',
],
theme: {
extend: {
screens: {
sm: {
max: '479px',
},
md: {
min: '480px',
max: '760px',
},
},
spacing: {
13: '3.25rem',
15: '3.75rem',
17: '4.25rem',
19: '4.75rem',
21: '5.25rem',
23: '5.75rem',
25: '6.25rem',
},
lineHeight: {
13: '3.25rem',
14: '3.5rem',
15: '3.75rem',
16: '4rem',
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
chart: {
1: 'hsl(var(--chart-1))',
2: 'hsl(var(--chart-2))',
3: 'hsl(var(--chart-3))',
4: 'hsl(var(--chart-4))',
5: 'hsl(var(--chart-5))',
},
},
borderRadius: {
xl: 'calc(var(--radius) + 4px)',
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [animate, setupInspiraUI, require('tailwindcss-animate')],
}

4
tsconfig.json

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
Loading…
Cancel
Save