mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 10:00:40 +08:00
refactor(dashboard): replace diamond bg with sci-fi cylinder panel
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="star-bg">
|
||||
<div class="bg">
|
||||
<canvas ref="canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,62 +10,83 @@ import { onMounted, onUnmounted, ref } from "vue";
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
let animId: number | null = null;
|
||||
|
||||
interface Star {
|
||||
interface Cylinder {
|
||||
x: number;
|
||||
y: number;
|
||||
s: number;
|
||||
phase: number;
|
||||
spd: number;
|
||||
}
|
||||
|
||||
const createGrid = (w: number, h: number): Star[] => {
|
||||
const stars: Star[] = [];
|
||||
const s = 28;
|
||||
const hStep = s * 0.866;
|
||||
const cols = Math.ceil(w / s) + 2;
|
||||
const rows = Math.ceil(h / hStep) + 2;
|
||||
const createGrid = (w: number, h: number): Cylinder[] => {
|
||||
const grid: Cylinder[] = [];
|
||||
const sx = 44, sy = 38;
|
||||
const cols = Math.ceil(w / sx) + 2;
|
||||
const rows = Math.ceil(h / sy) + 2;
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
stars.push({
|
||||
x: i * s + (j % 2) * (s / 2),
|
||||
y: j * hStep,
|
||||
s: 5 + Math.random() * 3,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
spd: 0.3 + Math.random() * 0.4,
|
||||
grid.push({
|
||||
x: i * sx + (j % 2) * (sx / 2),
|
||||
y: j * sy,
|
||||
phase: (i + j * 0.5) * 0.3,
|
||||
});
|
||||
}
|
||||
}
|
||||
return stars;
|
||||
return grid;
|
||||
};
|
||||
|
||||
const draw = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
s: number,
|
||||
a: number,
|
||||
up: boolean
|
||||
) => {
|
||||
const h = s * 0.5;
|
||||
const d = up ? 0 : h * 0.4;
|
||||
const draw = (ctx: CanvasRenderingContext2D, w: number, h: number, t: number) => {
|
||||
ctx.fillStyle = "#0c0e12";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y - h + d);
|
||||
ctx.lineTo(x + s * 0.5 + d, y);
|
||||
ctx.lineTo(x, y + h - d);
|
||||
ctx.lineTo(x - s * 0.5 - d, y);
|
||||
ctx.closePath();
|
||||
const sx = 44, sy = 38;
|
||||
const cols = Math.ceil(w / sx) + 2;
|
||||
const rows = Math.ceil(h / sy) + 2;
|
||||
|
||||
const g = ctx.createLinearGradient(x, y - h, x, y + h);
|
||||
g.addColorStop(0, `rgba(180, 210, 255, ${a})`);
|
||||
g.addColorStop(1, `rgba(80, 140, 220, ${a * 0.6})`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.fill();
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const x = i * sx + (j % 2) * (sx / 2);
|
||||
const y = j * sy;
|
||||
|
||||
ctx.strokeStyle = `rgba(220, 240, 255, ${a * 0.4})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
// Outer rim
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 8, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#1a1c22";
|
||||
ctx.fill();
|
||||
|
||||
// Recessed cylinder body
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 7, 0, Math.PI * 2);
|
||||
const bodyGrad = ctx.createRadialGradient(x, y - 2, 0, x, y, 7);
|
||||
bodyGrad.addColorStop(0, "#14161c");
|
||||
bodyGrad.addColorStop(1, "#0a0b0e");
|
||||
ctx.fillStyle = bodyGrad;
|
||||
ctx.fill();
|
||||
|
||||
// Sharp outer rim highlight
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 8, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "#2a2c34";
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Deep indigo glow from depth
|
||||
const depthPulse = 0.15 + 0.1 * Math.sin(t * 0.2);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||||
const glowGrad = ctx.createRadialGradient(x, y, 0, x, y, 4);
|
||||
glowGrad.addColorStop(0, `rgba(60, 40, 120, ${depthPulse})`);
|
||||
glowGrad.addColorStop(1, "transparent");
|
||||
ctx.fillStyle = glowGrad;
|
||||
ctx.fill();
|
||||
|
||||
// Starlight pinpoint at center
|
||||
const starPulse = 0.6 + 0.4 * Math.sin(t * 0.3 + (i + j) * 0.2);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(220, 230, 255, ${starPulse})`;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -73,28 +94,18 @@ onMounted(() => {
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let stars: Star[] = [];
|
||||
let t = 0;
|
||||
|
||||
const resize = () => {
|
||||
if (!canvas.value) return;
|
||||
canvas.value.width = window.innerWidth;
|
||||
canvas.value.height = window.innerHeight;
|
||||
stars = createGrid(canvas.value.width, canvas.value.height);
|
||||
};
|
||||
|
||||
const loop = () => {
|
||||
if (!canvas.value || !ctx) return;
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
t += 0.012;
|
||||
|
||||
stars.forEach((st) => {
|
||||
const fy = Math.sin(t * st.spd + st.phase) * 3;
|
||||
const up = Math.sin(t * st.spd + st.phase) > 0;
|
||||
const a = 0.2 + Math.abs(Math.sin(t * st.spd + st.phase)) * 0.5;
|
||||
draw(ctx, st.x, st.y + fy, st.s, a, up);
|
||||
});
|
||||
|
||||
draw(ctx, canvas.value.width, canvas.value.height, t);
|
||||
t += 0.015;
|
||||
animId = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
@@ -109,11 +120,11 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.star-bg {
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: #0a0e1a;
|
||||
background: #0c0e12;
|
||||
}
|
||||
|
||||
canvas {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "@/i18n/composables";
|
||||
|
||||
const { locale } = useI18n();
|
||||
@@ -84,13 +84,14 @@ const getQuoteOfTheDay = (): Quote => {
|
||||
return langQuotes[index];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
currentQuote.value = getQuoteOfTheDay();
|
||||
});
|
||||
|
||||
// Update quote when locale changes
|
||||
locale.value; // track dependency
|
||||
currentQuote.value = getQuoteOfTheDay();
|
||||
watch(
|
||||
locale,
|
||||
() => {
|
||||
currentQuote.value = getQuoteOfTheDay();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"login": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"defaultHint": "Default username: astrbot. Run `astrbot conf admin` before the first login.",
|
||||
"defaultHint": "Run `astrbot conf admin` to set up the administrator account first.",
|
||||
"shareLink": "Share Link",
|
||||
"linkCopied": "Link copied to clipboard",
|
||||
"linkCopyFailed": "Failed to copy link",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"login": "Вход",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"defaultHint": "Имя пользователя по умолчанию: astrbot. Перед первым входом выполните `astrbot conf admin`.",
|
||||
"defaultHint": "Сначала выполните `astrbot conf admin` для настройки учётной записи администратора.",
|
||||
"logo": {
|
||||
"title": "AstrBot Starlight Panel"
|
||||
"title": "AstrBot Звёздная Панель"
|
||||
},
|
||||
"theme": {
|
||||
"switchToDark": "Перейти на темную тему",
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"login": "登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"defaultHint": "默认账户为:astrbot,请先执行 `astrbot conf admin` 设置密码",
|
||||
"defaultHint": "请先运行 `astrbot conf admin` 设置管理员账号",
|
||||
"shareLink": "分享链接",
|
||||
"linkCopied": "链接已复制到剪贴板",
|
||||
"linkCopyFailed": "复制链接失败",
|
||||
"logo": {
|
||||
"title": "AstrBot Starlight Panel"
|
||||
"title": "AstrBot 星光面板"
|
||||
},
|
||||
"theme": {
|
||||
"switchToDark": "切换到深色主题",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useAuthStore } from "@/stores/auth";
|
||||
import { useApiStore } from "@/stores/api";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { useI18n, useModuleI18n } from "@/i18n/composables";
|
||||
import { useToast } from "@/utils/toast";
|
||||
import { getApiBaseUrlValidationError } from "@/utils/request";
|
||||
|
||||
@@ -17,6 +17,7 @@ const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const apiStore = useApiStore();
|
||||
const customizer = useCustomizerStore();
|
||||
const { locale } = useI18n();
|
||||
const { tm: t } = useModuleI18n("features/auth");
|
||||
const toast = useToast();
|
||||
|
||||
@@ -116,7 +117,7 @@ onMounted(() => {
|
||||
<div class="login-page-container">
|
||||
<DiamondBg />
|
||||
<v-card class="login-card" elevation="1">
|
||||
<v-card-title>
|
||||
<v-card-title :key="locale">
|
||||
<div class="d-flex justify-space-between align-center w-100">
|
||||
<img
|
||||
width="80"
|
||||
|
||||
Reference in New Issue
Block a user