mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
@@ -1,15 +1,16 @@
|
|||||||
FROM node:latest AS builder
|
FROM node:lts-alpine AS builder
|
||||||
|
|
||||||
RUN mkdir -p /workspace
|
RUN mkdir -p /workspace
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||||
|
RUN apk update && apk add git
|
||||||
RUN npm config set registry https://registry.npmmirror.com
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
RUN git clone https://github.com/setube/ogame-vue-ts.git
|
RUN git clone https://github.com/setube/ogame-vue-ts.git
|
||||||
RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/
|
RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/
|
||||||
|
|
||||||
RUN npm install -g pnpm ; pnpm install;
|
RUN npm install -g pnpm ; pnpm install;
|
||||||
RUN pnpm build
|
RUN pnpm run build
|
||||||
|
|
||||||
# --- 第二阶段:Nginx ---
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
@@ -23,18 +23,16 @@
|
|||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"finalhandler": "^2.1.1",
|
"finalhandler": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.556.0",
|
"lucide-vue-next": "^0.556.0",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
|
"motion-v": "^1.7.4",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"reka-ui": "^2.6.1",
|
"reka-ui": "^2.6.1",
|
||||||
"serve-static": "^2.2.0",
|
"serve-static": "^2.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "4",
|
"vue-router": "4",
|
||||||
@@ -46,10 +44,13 @@
|
|||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "npm:rolldown-vite@7.2.5",
|
"vite": "npm:rolldown-vite@7.2.5",
|
||||||
|
|||||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@@ -20,12 +20,6 @@ importers:
|
|||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||||
class-variance-authority:
|
|
||||||
specifier: ^0.7.1
|
|
||||||
version: 0.7.1
|
|
||||||
clsx:
|
|
||||||
specifier: ^2.1.1
|
|
||||||
version: 2.1.1
|
|
||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@@ -41,6 +35,9 @@ importers:
|
|||||||
marked:
|
marked:
|
||||||
specifier: ^17.0.1
|
specifier: ^17.0.1
|
||||||
version: 17.0.1
|
version: 17.0.1
|
||||||
|
motion-v:
|
||||||
|
specifier: ^1.7.4
|
||||||
|
version: 1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
||||||
@@ -53,9 +50,6 @@ importers:
|
|||||||
serve-static:
|
serve-static:
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
tailwind-merge:
|
|
||||||
specifier: ^3.4.0
|
|
||||||
version: 3.4.0
|
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
@@ -84,6 +78,12 @@ importers:
|
|||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: ^0.8.1
|
specifier: ^0.8.1
|
||||||
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
||||||
|
class-variance-authority:
|
||||||
|
specifier: ^0.7.1
|
||||||
|
version: 0.7.1
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
@@ -96,6 +96,9 @@ importers:
|
|||||||
electron-vite:
|
electron-vite:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
|
version: 5.0.0(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^3.4.0
|
||||||
|
version: 3.4.0
|
||||||
tw-animate-css:
|
tw-animate-css:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
@@ -2103,6 +2106,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fresh@2.0.0:
|
fresh@2.0.0:
|
||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2244,6 +2261,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hey-listen@1.0.8:
|
||||||
|
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
@@ -2772,6 +2792,18 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||||
|
|
||||||
|
motion-utils@12.23.6:
|
||||||
|
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||||
|
|
||||||
|
motion-v@1.7.4:
|
||||||
|
resolution: {integrity: sha512-YNDUAsany04wfI7YtHxQK3kxzNvh+OdFUk9GpA3+hMt7j6P+5WrVAAgr8kmPPoVza9EsJiAVhqoN3YYFN0Twrw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vueuse/core': '>=10.0.0'
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -6027,6 +6059,12 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
fs-extra@10.1.0:
|
fs-extra@10.1.0:
|
||||||
@@ -6208,6 +6246,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
hey-listen@1.0.8: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
hosted-git-info@4.1.0:
|
hosted-git-info@4.1.0:
|
||||||
@@ -6694,6 +6734,24 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
|
motion-utils@12.23.6: {}
|
||||||
|
|
||||||
|
motion-v@1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||||
|
framer-motion: 12.23.12
|
||||||
|
hey-listen: 1.0.8
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
vue: 3.5.25(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emotion/is-prop-valid'
|
||||||
|
- react
|
||||||
|
- react-dom
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|||||||
29
src/App.vue
29
src/App.vue
@@ -327,9 +327,30 @@
|
|||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<div class="animate-fade-in">
|
<Transition name="page" mode="out-in">
|
||||||
<RouterView />
|
<div :key="$route.fullPath" class="h-full">
|
||||||
</div>
|
<StarsBackground
|
||||||
|
v-if="isDark"
|
||||||
|
:factor="0.05"
|
||||||
|
:speed="50"
|
||||||
|
star-color="#fff"
|
||||||
|
class="h-full"
|
||||||
|
>
|
||||||
|
<RouterView class="h-full" />
|
||||||
|
</StarsBackground>
|
||||||
|
<div v-else class="h-full">
|
||||||
|
<ParticlesBg
|
||||||
|
class="absolute inset-0"
|
||||||
|
:quantity="100"
|
||||||
|
:ease="100"
|
||||||
|
:color="isDark ? '#FFF' : '#000'"
|
||||||
|
:staticity="10"
|
||||||
|
refresh
|
||||||
|
/>
|
||||||
|
<RouterView class="h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
@@ -453,6 +474,8 @@
|
|||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { migrateGameData } from '@/utils/migration'
|
import { migrateGameData } from '@/utils/migration'
|
||||||
import { checkLatestVersion } from '@/utils/versionCheck'
|
import { checkLatestVersion } from '@/utils/versionCheck'
|
||||||
|
import {StarsBackground} from "@/components/ui/bg-stars";
|
||||||
|
import {ParticlesBg} from "@/components/ui/particles-bg";
|
||||||
|
|
||||||
// 执行数据迁移(在 store 初始化之前)
|
// 执行数据迁移(在 store 初始化之前)
|
||||||
migrateGameData()
|
migrateGameData()
|
||||||
|
|||||||
92
src/assets/main.css
Normal file
92
src/assets/main.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.21 0.006 285.885);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.141 0.005 285.823);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.141 0.005 285.823);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.985 0 0);
|
||||||
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.274 0.006 286.033);
|
||||||
|
--input: oklch(0.274 0.006 286.033);
|
||||||
|
--ring: oklch(0.442 0.017 285.786);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
html.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
html.light {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
176
src/components/ui/bg-stars/StarsBackground.vue
Normal file
176
src/components/ui/bg-stars/StarsBackground.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
|
>
|
||||||
|
<motion.div :style="{ x: springX, y: springY }">
|
||||||
|
<!-- Star Layer 1 -->
|
||||||
|
<motion.div
|
||||||
|
class="absolute top-0 left-0 w-full h-[2000px]"
|
||||||
|
:animate="{ y: [0, -2000] }"
|
||||||
|
:transition="starLayer1Transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
boxShadow: boxShadow1,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full top-[2000px]"
|
||||||
|
:style="{
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
boxShadow: boxShadow1,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<!-- Star Layer 2 -->
|
||||||
|
<motion.div
|
||||||
|
class="absolute top-0 left-0 w-full h-[2000px]"
|
||||||
|
:animate="{ y: [0, -2000] }"
|
||||||
|
:transition="starLayer2Transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: '2px',
|
||||||
|
height: '2px',
|
||||||
|
boxShadow: boxShadow2,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full top-[2000px]"
|
||||||
|
:style="{
|
||||||
|
width: '2px',
|
||||||
|
height: '2px',
|
||||||
|
boxShadow: boxShadow2,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<!-- Star Layer 3 -->
|
||||||
|
<motion.div
|
||||||
|
class="absolute top-0 left-0 w-full h-[2000px]"
|
||||||
|
:animate="{ y: [0, -2000] }"
|
||||||
|
:transition="starLayer3Transition"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: '3px',
|
||||||
|
height: '3px',
|
||||||
|
boxShadow: boxShadow3,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full top-[2000px]"
|
||||||
|
:style="{
|
||||||
|
width: '3px',
|
||||||
|
height: '3px',
|
||||||
|
boxShadow: boxShadow3,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<!-- Slot for child content -->
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {SpringOptions, Transition} from "motion-v";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { motion, useMotionValue, useSpring } from "motion-v";
|
||||||
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
interface StarsBackgroundProps {
|
||||||
|
factor?: number;
|
||||||
|
speed?: number;
|
||||||
|
transition?: SpringOptions;
|
||||||
|
starColor?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
|
||||||
|
factor: 0.05,
|
||||||
|
speed: 50,
|
||||||
|
transition: () => ({ stiffness: 50, damping: 20 }),
|
||||||
|
starColor: "#fff",
|
||||||
|
});
|
||||||
|
|
||||||
|
// For slot content
|
||||||
|
defineSlots();
|
||||||
|
|
||||||
|
function generateStars(count: number, starColor: string) {
|
||||||
|
const shadows: string[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = Math.floor(Math.random() * 4000) - 2000;
|
||||||
|
const y = Math.floor(Math.random() * 4000) - 2000;
|
||||||
|
shadows.push(`${x}px ${y}px ${starColor}`);
|
||||||
|
}
|
||||||
|
return shadows.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetX = useMotionValue(1);
|
||||||
|
const offsetY = useMotionValue(1);
|
||||||
|
|
||||||
|
const springX = useSpring(offsetX, props.transition);
|
||||||
|
const springY = useSpring(offsetY, props.transition);
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
const centerX = window.innerWidth / 2;
|
||||||
|
const centerY = window.innerHeight / 2;
|
||||||
|
const newOffsetX = -(e.clientX - centerX) * props.factor;
|
||||||
|
const newOffsetY = -(e.clientY - centerY) * props.factor;
|
||||||
|
offsetX.set(newOffsetX);
|
||||||
|
offsetY.set(newOffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxShadow1 = ref("");
|
||||||
|
const boxShadow2 = ref("");
|
||||||
|
const boxShadow3 = ref("");
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
boxShadow1.value = generateStars(1000, props.starColor);
|
||||||
|
boxShadow2.value = generateStars(400, props.starColor);
|
||||||
|
boxShadow3.value = generateStars(200, props.starColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for starColor changes
|
||||||
|
watch(
|
||||||
|
() => props.starColor,
|
||||||
|
(newColor) => {
|
||||||
|
boxShadow1.value = generateStars(1000, newColor);
|
||||||
|
boxShadow2.value = generateStars(400, newColor);
|
||||||
|
boxShadow3.value = generateStars(200, newColor);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const starLayer1Transition = computed<Transition>(() => ({
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: props.speed,
|
||||||
|
ease: "linear" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const starLayer2Transition = computed<Transition>(() => ({
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: props.speed * 2,
|
||||||
|
ease: "linear" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const starLayer3Transition = computed<Transition>(() => ({
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: props.speed * 3,
|
||||||
|
ease: "linear" as const,
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
1
src/components/ui/bg-stars/index.ts
Normal file
1
src/components/ui/bg-stars/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as StarsBackground } from "./StarsBackground.vue";
|
||||||
250
src/components/ui/particles-bg/ParticlesBg.vue
Normal file
250
src/components/ui/particles-bg/ParticlesBg.vue
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="canvasContainerRef"
|
||||||
|
:class="$props.class"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<canvas ref="canvasRef"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMouse, useDevicePixelRatio } from "@vueuse/core";
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from "vue";
|
||||||
|
|
||||||
|
type Circle = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
translateX: number;
|
||||||
|
translateY: number;
|
||||||
|
size: number;
|
||||||
|
alpha: number;
|
||||||
|
targetAlpha: number;
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
magnetism: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
color?: string;
|
||||||
|
quantity?: number;
|
||||||
|
staticity?: number;
|
||||||
|
ease?: number;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
color: "#FFF",
|
||||||
|
quantity: 100,
|
||||||
|
staticity: 50,
|
||||||
|
ease: 50,
|
||||||
|
class: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const canvasContainerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const context = ref<CanvasRenderingContext2D | null>(null);
|
||||||
|
const circles = ref<Circle[]>([]);
|
||||||
|
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
|
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 });
|
||||||
|
const { x: mouseX, y: mouseY } = useMouse();
|
||||||
|
const { pixelRatio } = useDevicePixelRatio();
|
||||||
|
|
||||||
|
const color = computed(() => {
|
||||||
|
// Remove the leading '#' if it's present
|
||||||
|
let hex = props.color.replace(/^#/, "");
|
||||||
|
|
||||||
|
// If the hex code is 3 characters, expand it to 6 characters
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split("")
|
||||||
|
.map((char) => char + char)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the r, g, b values from the hex string
|
||||||
|
const bigint = parseInt(hex, 16);
|
||||||
|
const r = (bigint >> 16) & 255; // Extract the red component
|
||||||
|
const g = (bigint >> 8) & 255; // Extract the green component
|
||||||
|
const b = bigint & 255; // Extract the blue component
|
||||||
|
|
||||||
|
// Return the RGB values as a string separated by spaces
|
||||||
|
return `${r} ${g} ${b}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
context.value = canvasRef.value.getContext("2d");
|
||||||
|
}
|
||||||
|
|
||||||
|
initCanvas();
|
||||||
|
animate();
|
||||||
|
window.addEventListener("resize", initCanvas);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("resize", initCanvas);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([mouseX, mouseY], () => {
|
||||||
|
onMouseMove();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
resizeCanvas();
|
||||||
|
drawParticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove() {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
const rect = canvasRef.value.getBoundingClientRect();
|
||||||
|
const { w, h } = canvasSize;
|
||||||
|
const x = mouseX.value - rect.left - w / 2;
|
||||||
|
const y = mouseY.value - rect.top - h / 2;
|
||||||
|
|
||||||
|
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
||||||
|
if (inside) {
|
||||||
|
mouse.x = x;
|
||||||
|
mouse.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
if (canvasContainerRef.value && canvasRef.value && context.value) {
|
||||||
|
circles.value.length = 0;
|
||||||
|
canvasSize.w = canvasContainerRef.value.offsetWidth;
|
||||||
|
canvasSize.h = canvasContainerRef.value.offsetHeight;
|
||||||
|
canvasRef.value.width = canvasSize.w * pixelRatio.value;
|
||||||
|
canvasRef.value.height = canvasSize.h * pixelRatio.value;
|
||||||
|
canvasRef.value.style.width = canvasSize.w + "px";
|
||||||
|
canvasRef.value.style.height = canvasSize.h + "px";
|
||||||
|
context.value.scale(pixelRatio.value, pixelRatio.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function circleParams(): Circle {
|
||||||
|
const x = Math.floor(Math.random() * canvasSize.w);
|
||||||
|
const y = Math.floor(Math.random() * canvasSize.h);
|
||||||
|
const translateX = 0;
|
||||||
|
const translateY = 0;
|
||||||
|
const size = Math.floor(Math.random() * 2) + 1;
|
||||||
|
const alpha = 0;
|
||||||
|
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||||
|
const dx = (Math.random() - 0.5) * 0.2;
|
||||||
|
const dy = (Math.random() - 0.5) * 0.2;
|
||||||
|
const magnetism = 0.1 + Math.random() * 4;
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
translateX,
|
||||||
|
translateY,
|
||||||
|
size,
|
||||||
|
alpha,
|
||||||
|
targetAlpha,
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
magnetism,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCircle(circle: Circle, update = false) {
|
||||||
|
if (context.value) {
|
||||||
|
const { x, y, translateX, translateY, size, alpha } = circle;
|
||||||
|
context.value.translate(translateX, translateY);
|
||||||
|
context.value.beginPath();
|
||||||
|
context.value.arc(x, y, size, 0, 2 * Math.PI);
|
||||||
|
context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`;
|
||||||
|
context.value.fill();
|
||||||
|
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0);
|
||||||
|
|
||||||
|
if (!update) {
|
||||||
|
circles.value.push(circle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearContext() {
|
||||||
|
if (context.value) {
|
||||||
|
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawParticles() {
|
||||||
|
clearContext();
|
||||||
|
const particleCount = props.quantity;
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const circle = circleParams();
|
||||||
|
drawCircle(circle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapValue(
|
||||||
|
value: number,
|
||||||
|
start1: number,
|
||||||
|
end1: number,
|
||||||
|
start2: number,
|
||||||
|
end2: number,
|
||||||
|
): number {
|
||||||
|
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||||
|
return remapped > 0 ? remapped : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
clearContext();
|
||||||
|
circles.value.forEach((circle, i) => {
|
||||||
|
// Handle the alpha value
|
||||||
|
const edge = [
|
||||||
|
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||||
|
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||||
|
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||||
|
canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||||
|
];
|
||||||
|
|
||||||
|
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||||
|
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
|
||||||
|
|
||||||
|
if (remapClosestEdge > 1) {
|
||||||
|
circle.alpha += 0.02;
|
||||||
|
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
|
||||||
|
} else {
|
||||||
|
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||||
|
}
|
||||||
|
|
||||||
|
circle.x += circle.dx;
|
||||||
|
circle.y += circle.dy;
|
||||||
|
circle.translateX +=
|
||||||
|
(mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease;
|
||||||
|
circle.translateY +=
|
||||||
|
(mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease;
|
||||||
|
|
||||||
|
// circle gets out of the canvas
|
||||||
|
if (
|
||||||
|
circle.x < -circle.size ||
|
||||||
|
circle.x > canvasSize.w + circle.size ||
|
||||||
|
circle.y < -circle.size ||
|
||||||
|
circle.y > canvasSize.h + circle.size
|
||||||
|
) {
|
||||||
|
// remove the circle from the array
|
||||||
|
circles.value.splice(i, 1);
|
||||||
|
// create a new circle
|
||||||
|
const newCircle = circleParams();
|
||||||
|
drawCircle(newCircle);
|
||||||
|
// update the circle position
|
||||||
|
} else {
|
||||||
|
drawCircle(
|
||||||
|
{
|
||||||
|
...circle,
|
||||||
|
x: circle.x,
|
||||||
|
y: circle.y,
|
||||||
|
translateX: circle.translateX,
|
||||||
|
translateY: circle.translateY,
|
||||||
|
alpha: circle.alpha,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
1
src/components/ui/particles-bg/index.ts
Normal file
1
src/components/ui/particles-bg/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as ParticlesBg } from "./ParticlesBg.vue";
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ClassValue } from 'clsx'
|
import type { ClassValue } from "clsx"
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from "clsx"
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export const cn = (...inputs: ClassValue[]) => {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "@/assets/main.css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user