mirror of
https://github.com/rustfs/console.git
synced 2026-08-30 17:14:47 +08:00
fix: 修复主题问题
This commit is contained in:
@@ -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>
|
||||
|
||||
Vendored
+1
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
export { default as GithubGlobe } from './GithubGlobe.vue';
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<slot />
|
||||
<Ripple />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Ripple } from './Ripple.vue';
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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({
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+2326
-2251
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user