fix: 修复主题问题

This commit is contained in:
overtrue
2025-04-13 11:43:08 +08:00
parent a60ddbbc10
commit e4d92c7860
20 changed files with 2905 additions and 2450 deletions
+21 -8
View File
@@ -1,10 +1,6 @@
<template>
<div :class="theme.name === darkTheme.name ? 'dark' : ''">
<n-config-provider
:theme="theme"
:locale="locale"
:theme-overrides="themeOverrides"
:date-locale="dateLocale">
<div :class="isDark ? 'dark' : ''">
<n-config-provider :theme="theme" :locale="locale" :theme-overrides="themeOverrides" :date-locale="dateLocale">
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
@@ -18,18 +14,35 @@
</div>
</template>
<script lang="ts" setup>
import { useColorMode } from '@vueuse/core'
import {
darkTheme,
dateZhCN,
zhCN,
type GlobalTheme,
type NDateLocale,
type NLocale
} from 'naive-ui'
import { ref } from 'vue'
import { themeOverrides } from '~/config/theme'
const theme = ref<GlobalTheme>(darkTheme)
const { system, store } = useColorMode()
const isDark = computed(() => {
return store.value === 'dark' || (store.value === 'auto' && system.value === 'dark')
})
const themeName = computed(() => {
return store.value === 'auto' ? system.value : store.value
})
const theme = computed(() => {
if (isDark.value) {
return darkTheme
}
return { name: themeName.value }
})
const locale = ref<NLocale | null>(zhCN)
const dateLocale = ref<NDateLocale | null>(dateZhCN)
</script>
+1
View File
@@ -40,6 +40,7 @@ declare module 'vue' {
NGrid: typeof import('naive-ui')['NGrid']
NH3: typeof import('naive-ui')['NH3']
NH4: typeof import('naive-ui')['NH4']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8 overflow-hidden relative">
<div class="max-w-7xl flex flex-col z-10">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<span>基于 Rust </span>
<FlipWords :words="['高性能', '无限扩容', '安全可靠', '多云存储', '兼容 S3']" :duration="3000" class="text-4xl font-semibold !text-primary px-0" />
<div class="text-muted-foreground mt-2">
靠谱的分布式文件系统
</div>
</div>
</div>
<a href="https://www.rustfs.com" class="z-10 text-primary-500 inline-flex w-min items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<span>www.rustfs.com</span>
<Icon name="ri:arrow-right-long-fill" class="mr-2" />
</a>
<div class="h-full inset-0 absolute z-0">
<Ripple class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] [mask-image:linear-gradient(to_bottom,white,transparent)]"
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-full" />
</div>
</div>
</template>
<script lang="ts" setup>
import FlipWords from '~/components/ui/flip-words/FlipWords.vue'
import Ripple from '~/components/ui/ripple/Ripple.vue'
const mode = useColorMode();
</script>
+5 -4
View File
@@ -1,5 +1,5 @@
<template>
<div class="w-full bg-white dark:bg-black h-full p-16 flex flex-col justify-center gap-8">
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8">
<div class="max-w-7xl flex flex-col">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
@@ -10,8 +10,8 @@
</div>
</div>
</div>
<WorldMap class="absolute inset-0" :dots="dots" :map-color="isDark ? '#FFFFFF40' : '#00000040'" :map-bg-color="isDark ? 'black' : 'white'" />
<a href="https://www.rustfs.com" class="text-primary-500 inline-flex w-min items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<WorldMap class="absolute inset-0" :dots="dots" :map-color="isDark ? '#FFFFFF40' : '#00000040'" :map-bg-color="isDark ? 'black' : '#f9fafb'" />
<a href=" https://www.rustfs.com" class="text-primary-500 inline-flex w-min items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<span>www.rustfs.com</span>
<Icon name="ri:arrow-right-long-fill" class="mr-2" />
</a>
@@ -55,5 +55,6 @@ const dots = [
},
];
const isDark = computed(() => useColorMode().value == "dark");
const mode = useColorMode();
const isDark = computed(() => mode.value == "dark");
</script>
+18 -26
View File
@@ -1,4 +1,6 @@
<script lang="ts" setup>
import ClipboardJS from 'clipboard'
const props = defineProps({
readonly: {
type: Boolean,
@@ -8,51 +10,41 @@ const props = defineProps({
type: Boolean,
default: false,
},
id: {
type: String,
required: false,
default: () => {
return `copy-input-${Math.random().toString(36).substring(2, 15)}`;
},
},
});
const model = defineModel<string>();
const message = useMessage();
function handleCopy() {
function handleCopy() {
const value = model.value;
if (!value) {
message.error('当前无内容');
return;
}
// 首先检查Clipboard API是否可用
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(() => {
message.success(`复制成功:${value}`);
}).catch(err => {
message.error(`复制失败:${err}`);
try {
// @ts-ignore
ClipboardJS.copy(document.querySelector(`#${props.id}`)).then(() => {
message.success('复制成功');
});
} else {
// Clipboard API不可用,尝试使用document.execCommand
// 创建一个临时的textarea元素来选中文本,以便复制
let textarea = document.createElement('textarea');
textarea.value = value;
document.body.appendChild(textarea);
textarea.focus({preventScroll:true});
textarea.select();
try {
// 执行复制操作
document.execCommand('copy');
message.success(`复制成功:${value}`);
} catch (err) {
message.error(`复制失败:${err}`);
}
// 清理临时创建的textarea
document.body.removeChild(textarea);
} catch (error) {
message.error('复制失败')
console.error('复制失败', error);
}
}
</script>
<template>
<div class="h-full">
<NInputGroup>
<n-input v-model:value="model" :readonly="props.readonly" />
<n-input v-model:value="model" :readonly="props.readonly" :id="props.id" />
<n-input-group-label v-if="props.copyIcon" class="flex items-center" @click="handleCopy">
<Icon :size="25" name="ri:file-copy-line"></Icon>
</n-input-group-label>
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { Icon } from '#components'
import { useColorMode, useCycleList } from '@vueuse/core'
import { watchEffect } from 'vue'
const mode = useColorMode({
emitAuto: true,
})
const { state, next } = useCycleList(['dark', 'light', 'auto'] as const, { initialValue: mode })
watchEffect(() => mode.value = state.value)
const icons =
{
'dark': 'ri:moon-clear-fill',
'light': 'ri:sun-fill',
'auto': 'ri:computer-fill'
}
const switchTheme = () => {
next();
mode.value = state.value === "dark" ? "dark" : "light"
}
</script>
<template>
<n-button type="default" @click="switchTheme" class="theme-switcher">
<template #icon>
<n-icon>
<Icon :name="icons[state]" />
</n-icon>
</template>
</n-button>
</template>
+338
View File
@@ -0,0 +1,338 @@
<template>
<canvas
ref="githubGlobeRef"
:class="cn('w-96 h-96', props.class)"
></canvas>
</template>
<script lang="ts" setup>
// Download globe json file from https://geojson-maps.kyd.au/ and save in the same folder
/* eslint-disable @typescript-eslint/no-explicit-any */
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import ThreeGlobe from 'three-globe';
import {
AmbientLight,
Color,
DirectionalLight,
PerspectiveCamera,
PointLight,
Scene,
WebGLRenderer,
} from 'three';
import contries from './globe.json';
import { cn } from '@/lib/utils';
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
type Position = {
order: number;
startLat: number;
startLng: number;
endLat: number;
endLng: number;
arcAlt: number;
color: string;
};
interface GlobeData {
size: number | undefined;
order: number;
color: (t: number) => string;
lat: number;
lng: number;
}
interface GlobeConfig {
pointSize?: number;
globeColor?: string;
showAtmosphere?: boolean;
atmosphereColor?: string;
atmosphereAltitude?: number;
emissive?: string;
emissiveIntensity?: number;
shininess?: number;
polygonColor?: string;
ambientLight?: string;
directionalLeftLight?: string;
directionalTopLight?: string;
pointLight?: string;
arcTime?: number;
arcLength?: number;
rings?: number;
maxRings?: number;
initialPosition?: {
lat: number;
lng: number;
};
autoRotate?: boolean;
autoRotateSpeed?: number;
}
interface Props {
globeConfig?: GlobeConfig;
data?: Position[];
class?: string;
}
const props = withDefaults(defineProps<Props>(), {
globeConfig: () => {
return {};
},
data: () => [],
});
const defaultGlobeConfig: GlobeConfig = {
pointSize: 1,
atmosphereColor: '#ffffff',
showAtmosphere: true,
atmosphereAltitude: 0.1,
polygonColor: 'rgba(255,255,255,0.7)',
globeColor: '#1d072e',
emissive: '#000000',
emissiveIntensity: 0.1,
shininess: 0.9,
arcTime: 2000,
arcLength: 0.9,
rings: 1,
maxRings: 3,
...props.globeConfig,
};
const githubGlobeRef = ref<HTMLCanvasElement>();
const globeData = ref<GlobeData[]>();
let numberOfRings: number[] = [];
let renderer: WebGLRenderer;
let scene: Scene;
let camera: PerspectiveCamera;
let controls: OrbitControls;
let globe: ThreeGlobe;
onMounted(() => {
setupScene();
initGlobe();
startAnimation();
animate();
onWindowResize();
window.addEventListener('resize', onWindowResize, false);
watch(globeData, () => {
if (!globe || !globeData.value) return;
numberOfRings = genRandomNumbers(0, props.data.length, Math.floor((props.data.length * 4) / 5));
globe.ringsData(globeData.value.filter((d, i) => numberOfRings.includes(i)));
});
});
function setupScene() {
if (!githubGlobeRef.value) {
throw new Error('Canvas not initialized');
}
const width = githubGlobeRef.value.clientWidth;
const height = githubGlobeRef.value.clientHeight;
renderer = new WebGLRenderer({ canvas: githubGlobeRef.value, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(width, height);
renderer.autoClear = false;
scene = new Scene();
camera = new PerspectiveCamera();
camera.aspect = width / height;
camera.position.setX(0);
camera.position.setY(0);
camera.position.setZ(400);
const ambientLight = new AmbientLight(defaultGlobeConfig.ambientLight || '#ffffff', 0.6);
scene.add(ambientLight);
const dLight1 = new DirectionalLight(defaultGlobeConfig.directionalLeftLight || '#ffffff', 1);
dLight1.position.set(-400, 100, 400);
camera.add(dLight1);
const dLight2 = new DirectionalLight(defaultGlobeConfig.directionalTopLight || '#ffffff', 1);
dLight2.position.set(-200, 500, 200);
camera.add(dLight2);
const pLight = new PointLight(defaultGlobeConfig.pointLight || '#ffffff', 0.8);
pLight.position.set(-200, 500, 200);
camera.add(pLight);
camera.updateProjectionMatrix();
scene.add(camera);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;
controls.enablePan = false;
controls.enableDamping = true;
controls.dampingFactor = 0.01;
controls.minDistance = 200;
controls.maxDistance = 500;
controls.rotateSpeed = defaultGlobeConfig.autoRotateSpeed || 0.8;
controls.zoomSpeed = 1;
controls.autoRotate = defaultGlobeConfig.autoRotate || false;
controls.minPolarAngle = Math.PI / 3.5;
controls.maxPolarAngle = Math.PI - Math.PI / 3;
}
function initGlobe() {
buildData();
globe = new ThreeGlobe({
waitForGlobeReady: true,
animateIn: true,
})
.hexPolygonsData(contries.features)
.hexPolygonResolution(3)
.hexPolygonMargin(0.7)
.showAtmosphere(defaultGlobeConfig.showAtmosphere!)
.atmosphereColor(defaultGlobeConfig.atmosphereColor!)
.atmosphereAltitude(defaultGlobeConfig.atmosphereAltitude!)
.hexPolygonColor((e) => defaultGlobeConfig.polygonColor!);
globe.rotateY(-Math.PI * (5 / 9));
globe.rotateZ(-Math.PI / 6);
const globeMaterial = globe.globeMaterial() as unknown as {
color: Color;
emissive: Color;
emissiveIntensity: number;
shininess: number;
};
globeMaterial.color = new Color(defaultGlobeConfig.globeColor!);
globeMaterial.emissive = new Color(defaultGlobeConfig.emissive!);
globeMaterial.emissiveIntensity = defaultGlobeConfig.emissiveIntensity || 0.1;
globeMaterial.shininess = defaultGlobeConfig.shininess || 0.9;
scene.add(globe);
}
function onWindowResize() {
if (!githubGlobeRef.value) {
return;
}
const width = githubGlobeRef.value.clientWidth;
const height = githubGlobeRef.value.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
function startAnimation() {
if (!globe || !globeData.value!) return;
globe
.arcsData(props.data)
.arcStartLat((d: any) => d.startLat * 1)
.arcStartLng((d: any) => d.startLng * 1)
.arcEndLat((d: any) => d.endLat * 1)
.arcEndLng((d: any) => d.endLng * 1)
.arcColor((e: any) => e.color)
.arcAltitude((e: any) => e.arcAlt * 1)
.arcStroke((e: any) => [0.32, 0.28, 0.3][Math.round(Math.random() * 4)])
.arcDashLength(defaultGlobeConfig.arcLength!)
.arcDashInitialGap((e: any) => e.order * 1)
.arcDashGap(15)
.arcDashAnimateTime(defaultGlobeConfig.arcTime!)
.pointsData(props.data)
.pointColor((e: any) => e.color)
.pointsMerge(true)
.pointAltitude(0.0)
.pointRadius(2)
.ringsData([])
.ringColor((e: any) => (t: any) => e.color(t))
.ringMaxRadius(defaultGlobeConfig.maxRings!)
.ringPropagationSpeed(3)
.ringRepeatPeriod(
(defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / defaultGlobeConfig.rings!,
);
}
function animate() {
globe.rotation.y += 0.01; // Rotate globe
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function buildData() {
const arcs = props.data;
let points = [];
for (let i = 0; i < arcs.length; i++) {
const arc = arcs[i];
const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number };
points.push({
size: props.globeConfig.pointSize,
order: arc.order,
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
lat: arc.startLat,
lng: arc.startLng,
});
points.push({
size: props.globeConfig.pointSize,
order: arc.order,
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
lat: arc.endLat,
lng: arc.endLng,
});
}
// remove duplicates for same lat and lng
const filteredPoints = points.filter(
(v, i, a) =>
a.findIndex((v2) =>
['lat', 'lng'].every((k) => v2[k as 'lat' | 'lng'] === v[k as 'lat' | 'lng']),
) === i,
);
globeData.value = filteredPoints;
}
function hexToRgb(color: string) {
let hex = 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,
};
}
function genRandomNumbers(min: number, max: number, count: number) {
const arr = [];
while (arr.length < count) {
const r = Math.floor(Math.random() * (max - min)) + min;
if (arr.indexOf(r) === -1) arr.push(r);
}
return arr;
}
</script>
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export { default as GithubGlobe } from './GithubGlobe.vue';
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="absolute inset-0">
<RippleCircle v-for="index in numberOfCircles" :key="index" :opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
:size="baseCircleSize + index * spaceBetweenCircle" :animation-delay="index * waveSpeed" :border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
:class="circleClass" />
</div>
</template>
<script setup lang="ts">
import RippleCircle from './RippleCircle.vue'
interface Props {
baseCircleSize?: number;
baseCircleOpacity?: number;
spaceBetweenCircle?: number;
circleOpacityDowngradeRatio?: number;
circleClass?: string;
waveSpeed?: number;
numberOfCircles?: number;
}
withDefaults(defineProps<Props>(), {
baseCircleSize: 210,
baseCircleOpacity: 0.24,
circleOpacityDowngradeRatio: 0.03,
waveSpeed: 80,
spaceBetweenCircle: 70,
numberOfCircles: 7,
});
</script>
+46
View File
@@ -0,0 +1,46 @@
<template>
<div :class="cn('absolute shadow-xl', 'animate-ripple-circle', props.class)" />
</template>
<script setup lang="ts">
import { cn } from '@/lib/utils';
interface Props {
size?: number;
class?: string;
opacity?: number;
animationDelay?: number;
borderStyle?: string;
}
const props = withDefaults(defineProps<Props>(), {
size: 210,
opacity: 0.24,
});
</script>
<style scoped>
.animate-ripple-circle {
animation: ripple-effect var(--duration, 2s) ease-in-out calc(var(--i, 0) * 0.2s) infinite;
border-width: 1px;
top: 50%;
left: 50%;
width: v-bind('props.size + "px"');
height: v-bind('props.size + "px"');
animation-delay: v-bind('props.animationDelay + "ms"');
opacity: v-bind("props.opacity");
transform: translate(-50%, -50%) scale(1);
border-style: v-bind("props.borderStyle");
}
@keyframes ripple-effect {
0%,
100% {
transform: translate(-50%, -50%) scale(1);
}
50% {
transform: translate(-50%, -50%) scale(0.9);
}
}
</style>
+6
View File
@@ -0,0 +1,6 @@
<template>
<div class="relative">
<slot />
<Ripple />
</div>
</template>
+1
View File
@@ -0,0 +1 @@
export { default as Ripple } from './Ripple.vue';
+30 -127
View File
@@ -1,140 +1,43 @@
<template>
<div class="relative aspect-[2/1] w-full rounded-lg bg-white font-sans dark:bg-black">
<NuxtImg
:src="`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`"
class="pointer-events-none size-full select-none [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_90%,transparent)]"
alt="world map"
height="495"
width="1056"
:draggable="false"
/>
<svg
ref="svgRef"
view-box="0 0 800 400"
class="pointer-events-none absolute inset-0 size-full select-none"
>
<g
v-for="(dot, i) in props.dots"
:key="`path-group-${i}`"
>
<Motion
:key="`start-upper-${i}`"
as="path"
:d="createCurvedPath(dot)"
fill="none"
stroke="url(#path-gradient)"
stroke-width="1"
:initial="{
pathLength: 0,
}"
:animate="{
pathLength: 1,
}"
:transition="{
duration: 1,
delay: 0.5 * i,
ease: 'easeOut',
}"
></Motion>
<NuxtImg :src="`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`"
class="pointer-events-none size-full select-none [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]" alt="world map" height="495"
width="1056" :draggable="false" />
<svg ref="svgRef" view-box="0 0 800 400" class="pointer-events-none absolute inset-0 size-full select-none">
<g v-for="(dot, i) in props.dots" :key="`path-group-${i}`">
<Motion :key="`start-upper-${i}`" as="path" :d="createCurvedPath(dot)" fill="none" stroke="url(#path-gradient)" stroke-width="1" :initial="{
pathLength: 0,
}" :animate="{
pathLength: 1,
}" :transition="{
duration: 1,
delay: 0.5 * i,
ease: 'easeOut',
}"></Motion>
</g>
<defs>
<linearGradient
id="path-gradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop
offset="0%"
stop-color="white"
stop-opacity="0"
/>
<stop
offset="5%"
:stop-color="lineColor"
stop-opacity="1"
/>
<stop
offset="95%"
:stop-color="lineColor"
stop-opacity="1"
/>
<stop
offset="100%"
stop-color="white"
stop-opacity="0"
/>
<linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="white" stop-opacity="0" />
<stop offset="5%" :stop-color="lineColor" stop-opacity="1" />
<stop offset="95%" :stop-color="lineColor" stop-opacity="1" />
<stop offset="100%" stop-color="white" stop-opacity="0" />
</linearGradient>
</defs>
<g
v-for="(dot, i) in props.dots"
:key="`points-group-${i}`"
>
<g v-for="(dot, i) in props.dots" :key="`points-group-${i}`">
<g :key="`start-${i}`">
<circle
:cx="projectPoint(dot.start.lat, dot.start.lng).x"
:cy="projectPoint(dot.start.lat, dot.start.lng).y"
r="2"
:fill="props.lineColor"
/>
<circle
:cx="projectPoint(dot.start.lat, dot.start.lng).x"
:cy="projectPoint(dot.start.lat, dot.start.lng).y"
r="2"
:fill="props.lineColor"
opacity="0.5"
>
<animate
attribute-name="r"
from="2"
to="8"
dur="1.5s"
begin="0s"
repeat-count="indefinite"
/>
<animate
attribute-name="opacity"
from="0.5"
to="0"
dur="1.5s"
begin="0s"
repeat-count="indefinite"
/>
<circle :cx="projectPoint(dot.start.lat, dot.start.lng).x" :cy="projectPoint(dot.start.lat, dot.start.lng).y" r="2" :fill="props.lineColor" />
<circle :cx="projectPoint(dot.start.lat, dot.start.lng).x" :cy="projectPoint(dot.start.lat, dot.start.lng).y" r="2" :fill="props.lineColor" opacity="0.5">
<animate attribute-name="r" from="2" to="8" dur="1.5s" begin="0s" repeat-count="indefinite" />
<animate attribute-name="opacity" from="0.5" to="0" dur="1.5s" begin="0s" repeat-count="indefinite" />
</circle>
</g>
<g :key="`end-${i}`">
<circle
:cx="projectPoint(dot.end.lat, dot.end.lng).x"
:cy="projectPoint(dot.end.lat, dot.end.lng).y"
r="2"
:fill="props.lineColor"
/>
<circle
:cx="projectPoint(dot.end.lat, dot.end.lng).x"
:cy="projectPoint(dot.end.lat, dot.end.lng).y"
r="2"
:fill="props.lineColor"
opacity="0.5"
>
<animate
attribute-name="r"
from="2"
to="8"
dur="1.5s"
begin="0s"
repeat-count="indefinite"
/>
<animate
attribute-name="opacity"
from="0.5"
to="0"
dur="1.5s"
begin="0s"
repeat-count="indefinite"
/>
<circle :cx="projectPoint(dot.end.lat, dot.end.lng).x" :cy="projectPoint(dot.end.lat, dot.end.lng).y" r="2" :fill="props.lineColor" />
<circle :cx="projectPoint(dot.end.lat, dot.end.lng).x" :cy="projectPoint(dot.end.lat, dot.end.lng).y" r="2" :fill="props.lineColor" opacity="0.5">
<animate attribute-name="r" from="2" to="8" dur="1.5s" begin="0s" repeat-count="indefinite" />
<animate attribute-name="opacity" from="0.5" to="0" dur="1.5s" begin="0s" repeat-count="indefinite" />
</circle>
</g>
</g>
@@ -143,8 +46,8 @@
</template>
<script setup lang="ts">
import DottedMap from 'dotted-map';
import { Motion } from 'motion-v';
import DottedMap from 'dotted-map'
import { Motion } from 'motion-v'
interface Dot {
start: { lat: number; lng: number; label?: string };
+4 -5
View File
@@ -6,6 +6,7 @@ import type {
RadarSeriesOption,
} from 'echarts/charts'
// 组件类型的定义后缀都为 ComponentOption
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
import type {
DatasetComponentOption,
GridComponentOption,
@@ -14,7 +15,6 @@ import type {
ToolboxComponentOption,
TooltipComponentOption,
} from 'echarts/components'
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
import {
DatasetComponent, // 数据集组件
@@ -30,7 +30,6 @@ import * as echarts from 'echarts/core'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import { useTemplateRef } from 'vue'
import { useThemeStore } from '~/store/theme'
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = echarts.ComposeOption<
@@ -71,7 +70,7 @@ echarts.use([
export function useEcharts(ref: string, chartOptions: Ref<ECOption>) {
const el = useTemplateRef<HTMLLIElement>(ref)
const themeStore = useThemeStore()
const { system, store } = useColorMode()
let chart: echarts.ECharts | null = null
const { width, height } = useElementSize(el)
@@ -83,7 +82,7 @@ export function useEcharts(ref: string, chartOptions: Ref<ECOption>) {
if (!width || !height)
return
const chartTheme = themeStore.appTheme ? 'dark' : 'light'
const chartTheme = store.value == 'auto' ? system.value : store.value
await nextTick()
if (el) {
chart = echarts.init(el.value, chartTheme)
@@ -122,4 +121,4 @@ export function useEcharts(ref: string, chartOptions: Ref<ECOption>) {
destroy,
update,
}
}
}
+5 -3
View File
@@ -32,8 +32,10 @@ export default defineNuxtConfig({
viewport: 'width=device-width, initial-scale=1',
}
},
modules: ['@nuxtjs/tailwindcss', // '@nuxtjs/i18n',
'@pinia/nuxt', '@nuxt/icon', 'nuxtjs-naive-ui', '@vueuse/nuxt', '@nuxt/image'],
modules: [
'@nuxtjs/tailwindcss', // '@nuxtjs/i18n',
'@pinia/nuxt', '@nuxt/icon', 'nuxtjs-naive-ui', '@vueuse/nuxt', '@nuxt/image'
],
// Nuxt automatically reads the files in the plugins/ directory
plugins: [],
runtimeConfig: {
@@ -93,4 +95,4 @@ export default defineNuxtConfig({
})
]
}
})
})
+2326 -2251
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -20,6 +20,7 @@
"@nuxtjs/tailwindcss": "^6.12.2",
"@pinia/nuxt": "^0.9.0",
"@tanstack/vue-table": "^8.21.2",
"@types/three": "^0.175.0",
"@vueuse/integrations": "^12.0.0",
"aws-sdk": "^2.1692.0",
"axios": "^1.7.9",
@@ -36,8 +37,11 @@
"nuxt": "^3.15.4",
"nuxtjs-naive-ui": "^1.0.2",
"pinia": "^2.3.0",
"postprocessing": "^6.37.2",
"strip-json-comments": "^5.0.1",
"tailwindcss": "^3.4.17",
"three": "^0.175.0",
"three-globe": "^2.42.3",
"ufo": "^1.5.4",
"universal-cookie": "^7.2.2",
"vue": "^3.5.11",
+4 -2
View File
@@ -62,9 +62,9 @@ const handleLogin = async () => {
</AuroraBackground>
<div class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[75vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700">
<div class="hidden lg:block w-1/2">
<auth-heros-wordmap></auth-heros-wordmap>
<auth-heros-ripple></auth-heros-ripple>
</div>
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center dark:bg-neutral-900 dark:border-neutral-700 relative">
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative">
<div class="max-w-sm w-full p-4 sm:p-7">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="py-6">
@@ -125,6 +125,8 @@ const handleLogin = async () => {
登录遇到问题? <nuxt-link to="https://www.rustfs.com" class="text-blue-600 hover:underline">获取帮助</nuxt-link>
</p>
</div>
<theme-switcher />
</div>
</div>
</div>
-24
View File
@@ -1,24 +0,0 @@
import { defineStore } from "pinia";
export const useThemeStore = defineStore(
"theme",
() => {
const appTheme = ref();
const setTheme = (theme: "dark" | "light") => {
appTheme.value = theme;
};
const isDarkTheme = () => {
return appTheme.value === "dark";
};
watch(appTheme, (value, oldValue) => {
document.body.setAttribute("data-theme", value);
document.body.classList.remove(oldValue);
document.body.classList.add(value);
});
return { appTheme, setTheme, isDarkTheme };
}
);