前端:更换图标和优化echoes图标
This commit is contained in:
parent
79c66bbe69
commit
43e21e1d49
@ -38,9 +38,7 @@ impl AppState {
|
||||
*self.db.lock().await = Some(db);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +48,7 @@ impl AppState {
|
||||
|
||||
pub async fn trigger_restart(&self) -> CustomResult<()> {
|
||||
*self.restart_progress.lock().await = true;
|
||||
if let Ok(db) = self.sql_get().await{
|
||||
if let Ok(db) = self.sql_get().await {
|
||||
db.get_db().close().await?;
|
||||
}
|
||||
self.shutdown
|
||||
@ -61,7 +59,6 @@ impl AppState {
|
||||
.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[rocket::main]
|
||||
|
@ -28,7 +28,7 @@ impl std::fmt::Display for DatabaseType {
|
||||
|
||||
#[async_trait]
|
||||
pub trait DatabaseTrait: Send + Sync {
|
||||
async fn connect(database: &config::SqlConfig,db:bool) -> CustomResult<Self>
|
||||
async fn connect(database: &config::SqlConfig, db: bool) -> CustomResult<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
async fn execute_query<'a>(
|
||||
@ -67,9 +67,9 @@ impl Database {
|
||||
|
||||
pub async fn link(database: &config::SqlConfig) -> CustomResult<Self> {
|
||||
let db: Box<dyn DatabaseTrait> = match database.db_type.to_lowercase().as_str() {
|
||||
"postgresql" => Box::new(postgresql::Postgresql::connect(database,true).await?),
|
||||
"mysql" => Box::new(mysql::Mysql::connect(database,true).await?),
|
||||
"sqllite" => Box::new(sqllite::Sqlite::connect(database,true).await?),
|
||||
"postgresql" => Box::new(postgresql::Postgresql::connect(database, true).await?),
|
||||
"mysql" => Box::new(mysql::Mysql::connect(database, true).await?),
|
||||
"sqllite" => Box::new(sqllite::Sqlite::connect(database, true).await?),
|
||||
_ => return Err("unknown database type".into_custom_error()),
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ use super::{
|
||||
builder::{self, SafeValue},
|
||||
schema, DatabaseTrait,
|
||||
};
|
||||
use crate::common::error::{CustomResult,CustomErrorInto};
|
||||
use crate::common::error::{CustomErrorInto, CustomResult};
|
||||
use crate::config;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
@ -37,15 +37,16 @@ impl DatabaseTrait for Mysql {
|
||||
|
||||
let pool = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
MySqlPool::connect(&connection_str)
|
||||
).await.map_err(|_| "连接超时".into_custom_error())??;
|
||||
MySqlPool::connect(&connection_str),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "连接超时".into_custom_error())??;
|
||||
|
||||
if let Err(e) = pool.acquire().await{
|
||||
if let Err(e) = pool.acquire().await {
|
||||
pool.close().await;
|
||||
return Err(format!("数据库连接测试失败: {}", e).into_custom_error());
|
||||
return Err(format!("数据库连接测试失败: {}", e).into_custom_error());
|
||||
}
|
||||
|
||||
|
||||
Ok(Mysql { pool })
|
||||
}
|
||||
async fn execute_query<'a>(
|
||||
@ -102,7 +103,6 @@ impl DatabaseTrait for Mysql {
|
||||
);
|
||||
let grammar = schema::generate_schema(super::DatabaseType::MySQL, db_prefix)?;
|
||||
|
||||
|
||||
let pool = Self::connect(&db_config, false).await?.pool;
|
||||
|
||||
pool.execute(format!("CREATE DATABASE `{}`", db_config.db_name).as_str())
|
||||
|
@ -2,7 +2,7 @@ use super::{
|
||||
builder::{self, SafeValue},
|
||||
schema, DatabaseTrait,
|
||||
};
|
||||
use crate::common::error::{CustomResult,CustomErrorInto};
|
||||
use crate::common::error::{CustomErrorInto, CustomResult};
|
||||
use crate::config;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
@ -34,15 +34,16 @@ impl DatabaseTrait for Postgresql {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
let pool = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
PgPool::connect(&connection_str)
|
||||
).await.map_err(|_| "连接超时".into_custom_error())??;
|
||||
PgPool::connect(&connection_str),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "连接超时".into_custom_error())??;
|
||||
|
||||
if let Err(e) = pool.acquire().await{
|
||||
if let Err(e) = pool.acquire().await {
|
||||
pool.close().await;
|
||||
return Err(format!("数据库连接测试失败: {}", e).into_custom_error());
|
||||
return Err(format!("数据库连接测试失败: {}", e).into_custom_error());
|
||||
}
|
||||
|
||||
Ok(Postgresql { pool })
|
||||
|
@ -34,15 +34,16 @@ impl DatabaseTrait for Sqlite {
|
||||
|
||||
let pool = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
SqlitePool::connect(&connection_str)
|
||||
).await.map_err(|_| "连接超时".into_custom_error())??;
|
||||
SqlitePool::connect(&connection_str),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "连接超时".into_custom_error())??;
|
||||
|
||||
if let Err(e) = pool.acquire().await{
|
||||
if let Err(e) = pool.acquire().await {
|
||||
pool.close().await;
|
||||
return Err(format!("数据库连接测试失败: {}", e).into_custom_error());
|
||||
return Err(format!("数据库连接测试失败: {}", e).into_custom_error());
|
||||
}
|
||||
|
||||
|
||||
Ok(Sqlite { pool })
|
||||
}
|
||||
|
||||
|
@ -15,12 +15,12 @@ export const DEFAULT_CONFIG: EnvConfig = {
|
||||
VITE_API_BASE_URL: "http://127.0.0.1:22000",
|
||||
VITE_API_USERNAME: "",
|
||||
VITE_API_PASSWORD: "",
|
||||
VITE_PATTERN: "true"
|
||||
VITE_PATTERN: "true",
|
||||
} as const;
|
||||
|
||||
// 扩展 ImportMeta 接口
|
||||
declare global {
|
||||
interface ImportMetaEnv extends EnvConfig { }
|
||||
interface ImportMetaEnv extends EnvConfig {}
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
:root {
|
||||
--transition-duration: 150ms;
|
||||
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--logo-path-length: 1000;
|
||||
}
|
||||
|
||||
/* 基础过渡效果 */
|
||||
@ -16,102 +15,127 @@
|
||||
color var(--transition-duration) var(--transition-easing);
|
||||
}
|
||||
|
||||
/* 主题过渡效果 */
|
||||
.dark body,
|
||||
body {
|
||||
transition: background-color var(--transition-duration) var(--transition-easing);
|
||||
}
|
||||
|
||||
/* 基础布局样式 */
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 640px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo 动画 */
|
||||
@keyframes logo-anim {
|
||||
0% {
|
||||
stroke-dashoffset: var(--logo-path-length);
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
opacity: 0;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
5% {
|
||||
opacity: 1;
|
||||
stroke-dashoffset: var(--logo-path-length);
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
}
|
||||
|
||||
/* 慢速绘画过程 */
|
||||
45% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
/* 慢慢填充效果 */
|
||||
50% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
fill: currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 保持填充状态 */
|
||||
75% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
fill: currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 变回线条 */
|
||||
85% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
fill: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 线条消失 */
|
||||
95% {
|
||||
stroke-dashoffset: var(--logo-path-length);
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
fill: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: var(--logo-path-length);
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
fill: transparent;
|
||||
opacity: 0;
|
||||
}
|
||||
.animated-text {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#logo-anim,
|
||||
#logo-anim path {
|
||||
.animated-text path {
|
||||
fill: transparent;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-dashoffset: var(--logo-path-length);
|
||||
stroke-dasharray: var(--logo-path-length) var(--logo-path-length);
|
||||
stroke-dasharray: var(--path-length);
|
||||
stroke-dashoffset: var(--path-length);
|
||||
animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||
transform-origin: center;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* 确保 Logo 在暗色模式下的颜色正确 */
|
||||
.dark #logo-anim,
|
||||
.dark #logo-anim path {
|
||||
stroke: currentColor;
|
||||
@keyframes logo-anim {
|
||||
0% {
|
||||
stroke-dashoffset: var(--path-length);
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
fill: transparent;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
opacity: 1;
|
||||
stroke-dashoffset: var(--path-length);
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
60%, 75% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
fill: currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
85% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
fill: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
95% {
|
||||
stroke-dashoffset: var(--path-length);
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
fill: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: var(--path-length);
|
||||
stroke-dasharray: var(--path-length) var(--path-length);
|
||||
fill: transparent;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保在暗色模式下的颜色正确 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.animated-text path {
|
||||
stroke: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 先确保基本动画工作后再添加脉动效果 */
|
||||
.root {
|
||||
fill: none;
|
||||
stroke: var(--accent-9);
|
||||
stroke-width: 1px;
|
||||
stroke-linecap: round;
|
||||
opacity: 0;
|
||||
stroke-dasharray: 50;
|
||||
stroke-dashoffset: 50;
|
||||
animation: rootGrow 0.8s ease-out forwards var(--delay);
|
||||
}
|
||||
|
||||
@keyframes rootGrow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
stroke-dashoffset: 50;
|
||||
stroke-width: 0.5px;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes growPath {
|
||||
0% {
|
||||
stroke-dashoffset: 100%;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,20 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from "react";
|
||||
import {DEFAULT_CONFIG} from "app/env"
|
||||
import React, { createContext, useState } from "react";
|
||||
import { DEFAULT_CONFIG } from "app/env";
|
||||
import { HttpClient } from "core/http";
|
||||
import { ThemeModeToggle } from "hooks/themeMode";
|
||||
import { Theme, Button, Select, Flex, Container, Heading, Text, Box } from '@radix-ui/themes';
|
||||
import { toast } from 'hooks/notification';
|
||||
import { Echoes } from "hooks/echo";
|
||||
import {
|
||||
Theme,
|
||||
Button,
|
||||
Select,
|
||||
Flex,
|
||||
Container,
|
||||
Heading,
|
||||
Text,
|
||||
Box,
|
||||
TextField,
|
||||
} from "@radix-ui/themes";
|
||||
import { toast } from "hooks/notification";
|
||||
import { Echoes } from "hooks/echoes";
|
||||
|
||||
interface SetupContextType {
|
||||
currentStep: number;
|
||||
@ -25,27 +35,28 @@ const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({
|
||||
title,
|
||||
children,
|
||||
}) => (
|
||||
<Container size="2" className="w-full max-w-[90%] md:max-w-[600px] mx-auto px-4">
|
||||
<Heading size="5" mb="4">{title}</Heading>
|
||||
<Box style={{ width: "90%", maxWidth: "600px", margin: "0 auto" }}>
|
||||
<Heading size="5" mb="4" weight="bold">
|
||||
{title}
|
||||
</Heading>
|
||||
<Flex direction="column" gap="4">
|
||||
{children}
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
|
||||
// 通用的导航按钮组件
|
||||
const NavigationButtons: React.FC<StepProps & { loading?: boolean; disabled?: boolean }> = ({
|
||||
onNext,
|
||||
loading = false,
|
||||
disabled = false
|
||||
}) => (
|
||||
<Flex justify="end" mt="4" className="w-full">
|
||||
const NavigationButtons: React.FC<
|
||||
StepProps & { loading?: boolean; disabled?: boolean }
|
||||
> = ({ onNext, loading = false, disabled = false }) => (
|
||||
<Flex justify="end" mt="4">
|
||||
<Button
|
||||
onClick={onNext}
|
||||
size="3"
|
||||
disabled={loading || disabled}
|
||||
className="w-full md:w-auto"
|
||||
onClick={onNext}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{loading ? '处理中...' : '下一步'}
|
||||
{loading ? "处理中..." : "下一步"}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
@ -58,28 +69,31 @@ const InputField: React.FC<{
|
||||
hint?: string;
|
||||
required?: boolean;
|
||||
}> = ({ label, name, defaultValue, hint, required = true }) => (
|
||||
<Box mb="6" className="w-full">
|
||||
<Text as="label" size="2" weight="medium" mb="2" className="block">
|
||||
<Box mb="4">
|
||||
<Text as="label" size="2" weight="medium" className="block mb-2">
|
||||
{label} {required && <Text color="red">*</Text>}
|
||||
</Text>
|
||||
<input
|
||||
<TextField.Root
|
||||
name={name}
|
||||
defaultValue={defaultValue?.toString()}
|
||||
required={required}
|
||||
className="w-full h-[40px] px-3 rounded-md border border-gray-200 dark:border-gray-700 text-sm"
|
||||
/>
|
||||
{hint && <Text size="1" color="gray" mt="1" className="text-xs">{hint}</Text>}
|
||||
>
|
||||
<TextField.Slot></TextField.Slot>
|
||||
</TextField.Root>
|
||||
{hint && (
|
||||
<Text color="gray" size="1" mt="1">
|
||||
{hint}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
||||
<StepContainer title="安装说明">
|
||||
<div className="space-y-6">
|
||||
<p className="text-base text-custom-p-light dark:text-custom-p-dark">
|
||||
欢迎使用 Echoes
|
||||
</p>
|
||||
<NavigationButtons onNext={onNext} />
|
||||
</div>
|
||||
<Text size="3" style={{ lineHeight: 1.6 }}>
|
||||
欢迎使用 Echoes
|
||||
</Text>
|
||||
<NavigationButtons onNext={onNext} />
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
@ -91,11 +105,18 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
const validateForm = () => {
|
||||
const getRequiredFields = () => {
|
||||
switch (dbType) {
|
||||
case 'sqllite':
|
||||
return ['db_prefix', 'db_name'];
|
||||
case 'postgresql':
|
||||
case 'mysql':
|
||||
return ['db_host', 'db_prefix', 'db_port', 'db_user', 'db_password', 'db_name'];
|
||||
case "sqllite":
|
||||
return ["db_prefix", "db_name"];
|
||||
case "postgresql":
|
||||
case "mysql":
|
||||
return [
|
||||
"db_host",
|
||||
"db_prefix",
|
||||
"db_port",
|
||||
"db_user",
|
||||
"db_password",
|
||||
"db_name",
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@ -104,26 +125,35 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
const requiredFields = getRequiredFields();
|
||||
const emptyFields: string[] = [];
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const input = document.querySelector(`[name="${field}"]`) as HTMLInputElement;
|
||||
if (input && (!input.value || input.value.trim() === '')) {
|
||||
requiredFields.forEach((field) => {
|
||||
const input = document.querySelector(
|
||||
`[name="${field}"]`,
|
||||
) as HTMLInputElement;
|
||||
if (input && (!input.value || input.value.trim() === "")) {
|
||||
emptyFields.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
if (emptyFields.length > 0) {
|
||||
const fieldNames = emptyFields.map(field => {
|
||||
const fieldNames = emptyFields.map((field) => {
|
||||
switch (field) {
|
||||
case 'db_host': return '数据库地址';
|
||||
case 'db_prefix': return '数据库前缀';
|
||||
case 'db_port': return '端口';
|
||||
case 'db_user': return '用户名';
|
||||
case 'db_password': return '密码';
|
||||
case 'db_name': return '数据库名';
|
||||
default: return field;
|
||||
case "db_host":
|
||||
return "数据库地址";
|
||||
case "db_prefix":
|
||||
return "数据库前缀";
|
||||
case "db_port":
|
||||
return "端口";
|
||||
case "db_user":
|
||||
return "用户名";
|
||||
case "db_password":
|
||||
return "密码";
|
||||
case "db_name":
|
||||
return "数据库名";
|
||||
default:
|
||||
return field;
|
||||
}
|
||||
});
|
||||
toast.error(`请填写以下必填项:${fieldNames.join('、')}`);
|
||||
toast.error(`请填写以下必填项:${fieldNames.join("、")}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -139,27 +169,49 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
try {
|
||||
const formData = {
|
||||
db_type: dbType,
|
||||
host: (document.querySelector('[name="db_host"]') as HTMLInputElement)?.value?.trim()??"",
|
||||
db_prefix: (document.querySelector('[name="db_prefix"]') as HTMLInputElement)?.value?.trim()??"",
|
||||
port: Number((document.querySelector('[name="db_port"]') as HTMLInputElement)?.value?.trim()??0),
|
||||
user: (document.querySelector('[name="db_user"]') as HTMLInputElement)?.value?.trim()??"",
|
||||
password: (document.querySelector('[name="db_password"]') as HTMLInputElement)?.value?.trim()??"",
|
||||
db_name: (document.querySelector('[name="db_name"]') as HTMLInputElement)?.value?.trim()??"",
|
||||
host:
|
||||
(
|
||||
document.querySelector('[name="db_host"]') as HTMLInputElement
|
||||
)?.value?.trim() ?? "",
|
||||
db_prefix:
|
||||
(
|
||||
document.querySelector('[name="db_prefix"]') as HTMLInputElement
|
||||
)?.value?.trim() ?? "",
|
||||
port: Number(
|
||||
(
|
||||
document.querySelector('[name="db_port"]') as HTMLInputElement
|
||||
)?.value?.trim() ?? 0,
|
||||
),
|
||||
user:
|
||||
(
|
||||
document.querySelector('[name="db_user"]') as HTMLInputElement
|
||||
)?.value?.trim() ?? "",
|
||||
password:
|
||||
(
|
||||
document.querySelector('[name="db_password"]') as HTMLInputElement
|
||||
)?.value?.trim() ?? "",
|
||||
db_name:
|
||||
(
|
||||
document.querySelector('[name="db_name"]') as HTMLInputElement
|
||||
)?.value?.trim() ?? "",
|
||||
};
|
||||
|
||||
await http.post('/sql', formData);
|
||||
await http.post("/sql", formData);
|
||||
|
||||
let oldEnv = import.meta.env ?? DEFAULT_CONFIG;
|
||||
const viteEnv = Object.entries(oldEnv).reduce((acc, [key, value]) => {
|
||||
if (key.startsWith('VITE_')) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
const viteEnv = Object.entries(oldEnv).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key.startsWith("VITE_")) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
const newEnv = {
|
||||
...viteEnv,
|
||||
VITE_INIT_STATUS: '2'
|
||||
VITE_INIT_STATUS: "2",
|
||||
};
|
||||
|
||||
await http.dev("/env", {
|
||||
@ -169,7 +221,7 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
|
||||
Object.assign(import.meta.env, newEnv);
|
||||
|
||||
toast.success('数据库配置成功!');
|
||||
toast.success("数据库配置成功!");
|
||||
|
||||
setTimeout(() => onNext(), 1000);
|
||||
} catch (error: any) {
|
||||
@ -312,14 +364,12 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
interface InstallReplyData {
|
||||
token: string,
|
||||
username: string,
|
||||
password: string,
|
||||
token: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
||||
const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const http = HttpClient.getInstance();
|
||||
@ -328,29 +378,41 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formData = {
|
||||
username: (document.querySelector('[name="admin_username"]') as HTMLInputElement)?.value,
|
||||
password: (document.querySelector('[name="admin_password"]') as HTMLInputElement)?.value,
|
||||
email: (document.querySelector('[name="admin_email"]') as HTMLInputElement)?.value,
|
||||
username: (
|
||||
document.querySelector('[name="admin_username"]') as HTMLInputElement
|
||||
)?.value,
|
||||
password: (
|
||||
document.querySelector('[name="admin_password"]') as HTMLInputElement
|
||||
)?.value,
|
||||
email: (
|
||||
document.querySelector('[name="admin_email"]') as HTMLInputElement
|
||||
)?.value,
|
||||
};
|
||||
|
||||
const response = await http.post('/administrator', formData) as InstallReplyData;
|
||||
const response = (await http.post(
|
||||
"/administrator",
|
||||
formData,
|
||||
)) as InstallReplyData;
|
||||
const data = response;
|
||||
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem("token", data.token);
|
||||
|
||||
let oldEnv = import.meta.env ?? DEFAULT_CONFIG;
|
||||
const viteEnv = Object.entries(oldEnv).reduce((acc, [key, value]) => {
|
||||
if (key.startsWith('VITE_')) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
const viteEnv = Object.entries(oldEnv).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key.startsWith("VITE_")) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
const newEnv = {
|
||||
...viteEnv,
|
||||
VITE_INIT_STATUS: '3',
|
||||
VITE_INIT_STATUS: "3",
|
||||
VITE_API_USERNAME: data.username,
|
||||
VITE_API_PASSWORD: data.password
|
||||
VITE_API_PASSWORD: data.password,
|
||||
};
|
||||
|
||||
await http.dev("/env", {
|
||||
@ -360,7 +422,7 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
|
||||
Object.assign(import.meta.env, newEnv);
|
||||
|
||||
toast.success('管理员账号创建成功!');
|
||||
toast.success("管理员账号创建成功!");
|
||||
onNext();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@ -382,62 +444,46 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SetupComplete: React.FC = () => {
|
||||
|
||||
return (
|
||||
<StepContainer title="安装完成">
|
||||
<div className="text-center">
|
||||
<p className="text-xl text-custom-p-light dark:text-custom-p-dark mb-4">
|
||||
恭喜!安装已完成
|
||||
</p>
|
||||
<p className="text-base text-custom-p-light dark:text-custom-p-dark">
|
||||
系统正在重启中,请稍候...
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</StepContainer>
|
||||
);
|
||||
};
|
||||
const SetupComplete: React.FC = () => (
|
||||
<StepContainer title="安装完成">
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<Text size="5" weight="medium">
|
||||
恭喜!安装已完成
|
||||
</Text>
|
||||
<Text size="3">系统正在重启中,请稍候...</Text>
|
||||
<Box mt="4">
|
||||
<Flex justify="center">
|
||||
<Box className="animate-spin rounded-full h-8 w-8 border-b-2 border-current"></Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
export default function SetupPage() {
|
||||
const [currentStep, setCurrentStep] = useState(() => {
|
||||
return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1;
|
||||
});
|
||||
|
||||
const [appearance, setAppearance] = useState<'light' | 'dark'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
// 在客户端运行时检查主题
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
setAppearance(isDark ? 'dark' : 'light');
|
||||
|
||||
// 监听主题变化
|
||||
const handleThemeChange = (event: CustomEvent<{ theme: 'light' | 'dark' }>) => {
|
||||
setAppearance(event.detail.theme);
|
||||
};
|
||||
|
||||
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
||||
return () => window.removeEventListener('theme-change', handleThemeChange as EventListener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Theme
|
||||
accentColor="blue"
|
||||
grayColor="slate"
|
||||
grayColor="gray"
|
||||
accentColor="gray"
|
||||
radius="medium"
|
||||
appearance={appearance}
|
||||
panelBackground="solid"
|
||||
appearance="inherit"
|
||||
>
|
||||
<div className="min-h-screen w-full">
|
||||
<div className="fixed top-2 right-4 z-10">
|
||||
<Box className="min-h-screen w-full">
|
||||
<Box position="fixed" top="2" right="4">
|
||||
<ThemeModeToggle />
|
||||
</div>
|
||||
<div className="flex justify-center pt-2">
|
||||
<div className="w-20 h-20 md:w-24 md:h-24">
|
||||
</Box>
|
||||
|
||||
<Flex justify="center" pt="2">
|
||||
<Box className="w-20 h-20">
|
||||
<Echoes />
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" className="min-h-screen w-full pb-4">
|
||||
<Container className="w-full">
|
||||
<SetupContext.Provider value={{ currentStep, setCurrentStep }}>
|
||||
@ -450,15 +496,13 @@ export default function SetupPage() {
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<AdminConfig
|
||||
onNext={() => setCurrentStep(currentStep + 1)}
|
||||
/>
|
||||
<AdminConfig onNext={() => setCurrentStep(currentStep + 1)} />
|
||||
)}
|
||||
{currentStep === 4 && <SetupComplete />}
|
||||
</SetupContext.Provider>
|
||||
</Container>
|
||||
</Flex>
|
||||
</div>
|
||||
</Box>
|
||||
</Theme>
|
||||
);
|
||||
}
|
11
frontend/app/page.tsx
Normal file
11
frontend/app/page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import GrowingTree from "../hooks/tide";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div style={{ position: "relative", minHeight: "100vh" }}>
|
||||
<GrowingTree />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -6,74 +6,42 @@ import {
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
import { NotificationProvider } from "hooks/notification";
|
||||
import { Theme } from '@radix-ui/themes';
|
||||
import { Theme } from "@radix-ui/themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import "~/index.css";
|
||||
|
||||
export function Layout() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化主题
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
setTheme(isDark ? 'dark' : 'light');
|
||||
|
||||
// 监听主题变化
|
||||
const handleThemeChange = (event: CustomEvent<{ theme: 'light' | 'dark' }>) => {
|
||||
setTheme(event.detail.theme);
|
||||
};
|
||||
|
||||
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
||||
return () => window.removeEventListener('theme-change', handleThemeChange as EventListener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className="h-full"
|
||||
suppressHydrationWarning={true}
|
||||
>
|
||||
<html lang="en" className="h-full light" suppressHydrationWarning={true}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<meta
|
||||
name="generator"
|
||||
content="echoes"
|
||||
/>
|
||||
<Meta />
|
||||
<Links />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="generator" content="echoes" />
|
||||
<title>Echoes</title>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
document.documentElement.classList.remove('dark');
|
||||
const savedTheme = localStorage.getItem('theme-preference');
|
||||
if (savedTheme) {
|
||||
document.documentElement.classList.toggle('dark', savedTheme === 'dark');
|
||||
} else {
|
||||
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
document.documentElement.classList.toggle('dark', darkModeMediaQuery.matches);
|
||||
function getInitialTheme() {
|
||||
const savedTheme = localStorage.getItem('theme-preference');
|
||||
if (savedTheme) return savedTheme;
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
}
|
||||
})()
|
||||
|
||||
document.documentElement.className = getInitialTheme();
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body
|
||||
className="h-full"
|
||||
suppressHydrationWarning={true}
|
||||
>
|
||||
<Theme
|
||||
appearance={theme}
|
||||
accentColor="blue"
|
||||
grayColor="slate"
|
||||
radius="medium"
|
||||
scaling="100%"
|
||||
>
|
||||
<body className="h-full" suppressHydrationWarning={true}>
|
||||
<Theme grayColor="slate" radius="medium" scaling="100%">
|
||||
<NotificationProvider>
|
||||
<Outlet />
|
||||
</NotificationProvider>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import ErrorPage from 'hooks/error';
|
||||
import Layout from 'themes/echoes/layout';
|
||||
import ErrorPage from "hooks/error";
|
||||
import Layout from "themes/echoes/layout";
|
||||
|
||||
export default function Routes() {
|
||||
return Layout.render({
|
||||
children: <></>,
|
||||
args: {
|
||||
title: "我的页面",
|
||||
theme: "dark"
|
||||
}
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export class CapabilityService {
|
||||
|
||||
private static instance: CapabilityService;
|
||||
|
||||
private constructor() { }
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): CapabilityService {
|
||||
if (!this.instance) {
|
||||
|
@ -50,16 +50,15 @@ export class HttpClient {
|
||||
console.error("解析响应错误:", e);
|
||||
}
|
||||
|
||||
switch (response.status){
|
||||
switch (response.status) {
|
||||
case 404:
|
||||
message="请求的资源不存在";
|
||||
break
|
||||
message = "请求的资源不存在";
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const errorResponse: ErrorResponse = {
|
||||
title: `${response.status} ${response.statusText}`,
|
||||
message: message
|
||||
message: message,
|
||||
};
|
||||
|
||||
throw errorResponse;
|
||||
@ -71,7 +70,6 @@ export class HttpClient {
|
||||
: response.text();
|
||||
}
|
||||
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
@ -98,7 +96,7 @@ export class HttpClient {
|
||||
if (error.name === "AbortError") {
|
||||
const errorResponse: ErrorResponse = {
|
||||
title: "请求超时",
|
||||
message: "服务器响应时间过长,请稍后重试"
|
||||
message: "服务器响应时间过长,请稍后重试",
|
||||
};
|
||||
throw errorResponse;
|
||||
}
|
||||
@ -109,7 +107,7 @@ export class HttpClient {
|
||||
|
||||
const errorResponse: ErrorResponse = {
|
||||
title: "未知错误",
|
||||
message: error.message || "发生未知错误"
|
||||
message: error.message || "发生未知错误",
|
||||
};
|
||||
throw errorResponse;
|
||||
} finally {
|
||||
@ -161,12 +159,14 @@ export class HttpClient {
|
||||
}
|
||||
|
||||
public async systemToken<T>(): Promise<T> {
|
||||
|
||||
const formData = {
|
||||
"username": import.meta.env.VITE_API_USERNAME,
|
||||
"password": import.meta.env.VITE_API_PASSWORD
|
||||
}
|
||||
username: import.meta.env.VITE_API_USERNAME,
|
||||
password: import.meta.env.VITE_API_PASSWORD,
|
||||
};
|
||||
|
||||
return this.api<T>("/auth/token/system", { method: "POST",body: JSON.stringify(formData), });
|
||||
return this.api<T>("/auth/token/system", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,5 @@ export class TemplateManager {
|
||||
return TemplateManager.instance;
|
||||
}
|
||||
|
||||
// 读取主题和模板中的模板
|
||||
|
||||
// 读取主题和模板中的模板
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const Echoes: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 233 62"
|
||||
className="w-full h-full"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
id="logo-anim"
|
||||
style={{
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
fillRule: "evenodd",
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M0 0 C4.55555556 0.55555556 4.55555556 0.55555556 6 2 C7.13201767 10.83574853 4.5537396 19.48771208 2.5 28 C1.99804478 30.12284226 1.49918528 32.24637956 1.0020752 34.37036133 C0.55430717 36.27682919 0.09865509 38.18144027 -0.35742188 40.0859375 C-1.07177874 42.82816851 -1.07177874 42.82816851 -1 45 C-0.48050781 44.20335937 0.03898437 43.40671875 0.57421875 42.5859375 C11.10252879 26.5891337 11.10252879 26.5891337 17 24 C20.125 23.5 20.125 23.5 23 24 C26.83465936 28.76741433 27.00477728 34.3725597 27.72851562 40.23046875 C28.50735742 45.71462102 30.11315491 49.28422199 33 54 C32.75 56.4375 32.75 56.4375 32 58 C28.7049067 58.29955394 26.64500567 58.37625331 23.75 56.6875 C21.00094229 52.46573281 20.72390713 47.9186904 20 43 C19.63700368 41.10183175 19.26280756 39.20575496 18.875 37.3125 C18.70742187 36.48363281 18.53984375 35.65476563 18.3671875 34.80078125 C18.24601562 34.20652344 18.12484375 33.61226562 18 33 C14.4685318 34.44016491 12.92200112 36.10397858 10.8125 39.25 C10.21695313 40.13171875 9.62140625 41.0134375 9.0078125 41.921875 C6.31698552 46.04711169 3.6455625 50.18491771 1.02075195 54.35253906 C-0.52125135 56.78724519 -1.95491094 58.95491094 -4 61 C-6.5625 61.1875 -6.5625 61.1875 -9 61 C-10 60 -10 60 -10.14013672 56.47949219 C-10.08148221 52.01604551 -9.48822252 47.72494726 -8.7578125 43.328125 C-8.56516312 42.12261993 -8.56516312 42.12261993 -8.36862183 40.89276123 C-7.95990355 38.34419537 -7.54258548 35.79712628 -7.125 33.25 C-6.85230051 31.55738245 -6.58016064 29.86467464 -6.30859375 28.171875 C-5.78582 24.9187608 -5.2557761 21.66696722 -4.71972656 18.41601562 C-4.26905092 15.67719239 -3.84070507 12.9385784 -3.42871094 10.19335938 C-3.2640332 9.11376953 -3.09935547 8.03417969 -2.9296875 6.921875 C-2.79546387 6.0050293 -2.66124023 5.08818359 -2.52294922 4.14355469 C-2 2 -2 2 0 0 Z "
|
||||
transform="translate(87,0)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C2.125 1.5 2.125 1.5 3 4 C3.16869423 9.06082702 2.39778026 12.02220803 -0.75 16 C-4.92335377 20.45332718 -10.07929122 23.19240511 -15.40234375 26.04296875 C-18.24112837 27.73840784 -18.24112837 27.73840784 -18.56640625 30.61328125 C-18.2014155 33.35790574 -18.2014155 33.35790574 -15 35 C-7.97649839 35 -4.17909427 32.58337686 1 28 C1.66 27.34 2.32 26.68 3 26 C5.125 26.375 5.125 26.375 7 27 C5.47373298 34.12257944 0.95607598 36.98449365 -4.75 40.875 C-10.84193697 42.98374741 -16.67866319 43.05885539 -22.5625 40.375 C-26.24444983 36.78745914 -26.83021954 33.76571676 -27.3125 28.75 C-26.44034596 18.28415152 -22.13508157 9.50933046 -14.23828125 2.41796875 C-9.87287645 -0.46177381 -5.0404331 -0.96390753 0 0 Z M-13.0859375 11.05078125 C-15.46908589 13.47770175 -16.88035317 15.79034576 -18 19 C-17.67 19.66 -17.34 20.32 -17 21 C-15.20386168 19.73549459 -13.41322966 18.46316512 -11.625 17.1875 C-10.62726563 16.47980469 -9.62953125 15.77210937 -8.6015625 15.04296875 C-5.83319352 13.08757661 -5.83319352 13.08757661 -4 10 C-4.30840599 7.84725052 -4.30840599 7.84725052 -5 6 C-8.48529312 6 -10.5885008 8.81216981 -13.0859375 11.05078125 Z "
|
||||
transform="translate(194,19)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C2.125 1.5 2.125 1.5 3 4 C3.16869423 9.06082702 2.39778026 12.02220803 -0.75 16 C-4.92335377 20.45332718 -10.07929122 23.19240511 -15.40234375 26.04296875 C-18.24112837 27.73840784 -18.24112837 27.73840784 -18.56640625 30.61328125 C-18.2014155 33.35790574 -18.2014155 33.35790574 -15 35 C-7.97649839 35 -4.17909427 32.58337686 1 28 C1.66 27.34 2.32 26.68 3 26 C5.125 26.375 5.125 26.375 7 27 C5.47373298 34.12257944 0.95607598 36.98449365 -4.75 40.875 C-10.84193697 42.98374741 -16.67866319 43.05885539 -22.5625 40.375 C-26.24444983 36.78745914 -26.83021954 33.76571676 -27.3125 28.75 C-26.44034596 18.28415152 -22.13508157 9.50933046 -14.23828125 2.41796875 C-9.87287645 -0.46177381 -5.0404331 -0.96390753 0 0 Z M-13.0859375 11.05078125 C-15.46908589 13.47770175 -16.88035317 15.79034576 -18 19 C-17.67 19.66 -17.34 20.32 -17 21 C-15.20386168 19.73549459 -13.41322966 18.46316512 -11.625 17.1875 C-10.62726562 16.47980469 -9.62953125 15.77210937 -8.6015625 15.04296875 C-5.83319352 13.08757661 -5.83319352 13.08757661 -4 10 C-4.30840599 7.84725052 -4.30840599 7.84725052 -5 6 C-8.48529312 6 -10.5885008 8.81216981 -13.0859375 11.05078125 Z "
|
||||
transform="translate(27,19)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C3.69527184 1.74685578 5.47717274 3.28546941 7.875 6.5625 C10.114188 13.40446332 9.97220755 19.67980807 6.9375 26.25 C3.26647723 32.77280599 -1.74359246 38.58119749 -9 41 C-13.16700248 41.60697745 -17.12242085 41.98601239 -20.94140625 39.98046875 C-24.51875377 37.12721085 -25.79629249 34.79211037 -26.30859375 30.28515625 C-26.65858689 21.53532781 -25.02308905 15.22860505 -20 8 C-13.78847728 1.31686166 -9.14425293 -0.99729947 0 0 Z M-13 10 C-17.99946227 16.38820179 -19.51248523 22.05647893 -19 30 C-18.49166762 32.44162567 -18.49166762 32.44162567 -17 34 C-14.78705637 35.19488739 -14.78705637 35.19488739 -12 35 C-5.3983905 32.0528529 -0.86192365 27.77490776 2 21 C2.58594139 16.74621767 2.38401583 13.0240422 0.875 9 C-0.9152847 6.79591315 -0.9152847 6.79591315 -3.8125 5.9375 C-8.0777564 6.02113248 -9.76655628 7.25860206 -13 10 Z "
|
||||
transform="translate(151,21)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C3.08616928 3.08616928 4.90831127 5.56354329 5.1875 9.9375 C5 13 5 13 3 16 C1.1875 15.9375 1.1875 15.9375 -1 15 C-2.75 11.4375 -2.75 11.4375 -4 8 C-10.71628357 11.50067752 -13.45658865 17.42209188 -16.375 24.1875 C-17.13192649 27.5936692 -17.3342212 30.52409948 -17 34 C-13.63485541 36.24342972 -12.10349913 36.20088223 -8.1875 35.625 C-4.98478661 34.70993903 -2.55692219 32.94846434 0.16796875 31.078125 C2.52392749 29.69167593 4.31957201 29.58118313 7 30 C6.52159393 34.30565461 4.20810194 36.26970047 1 39 C-4.98635827 43.19452314 -10.80628431 43.92458415 -18 43 C-21.31141732 41.71598104 -22.4693361 40.85917012 -24.3125 37.875 C-26.28793198 29.61410261 -24.71328334 22.56854516 -21 15 C-16.30491914 7.59711261 -9.74998405 -2.0235816 0 0 Z "
|
||||
transform="translate(64,19)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C0.77601563 -0.00515625 1.55203125 -0.0103125 2.3515625 -0.015625 C4.375 0.25 4.375 0.25 6.375 2.25 C6 4.875 6 4.875 5.375 7.25 C4.80652344 7.27578125 4.23804688 7.3015625 3.65234375 7.328125 C-2.13318736 7.75310558 -5.86965905 8.88163349 -10.625 12.25 C-10.955 13.24 -11.285 14.23 -11.625 15.25 C-8.06916346 19.26465416 -4.48770953 21.60991904 0.2890625 24.0390625 C3.33050964 25.8046966 5.02772758 27.30740343 6.25 30.625 C6.41854678 34.16448233 5.57116559 35.487082 3.375 38.25 C-4.08131256 44.18115772 -12.33738592 45.20835775 -21.625 44.25 C-22.285 43.59 -22.945 42.93 -23.625 42.25 C-23.08928571 38.71428571 -23.08928571 38.71428571 -21.625 37.25 C-20.12443322 37.25595463 -18.62397304 37.3056031 -17.125 37.375 C-11.80066201 37.45171957 -7.52448551 36.23819702 -2.625 34.25 C-2.625 33.26 -2.625 32.27 -2.625 31.25 C-5.18015457 29.45648905 -5.18015457 29.45648905 -8.375 27.8125 C-17.82201749 22.71247969 -17.82201749 22.71247969 -19.5625 17.375 C-19.64363503 13.31824856 -18.93117065 11.60443004 -16.625 8.25 C-11.72056199 3.88588143 -6.7658008 -0.04525619 0 0 Z "
|
||||
transform="translate(226.625,17.75)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
43
frontend/hooks/echoes.tsx
Normal file
43
frontend/hooks/echoes.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
export const Echoes: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
className="animated-text w-full h-full"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="50.4 44.600006 234.1 86"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M 77.6 116.50001 Q 75.5 118.30001 72.75 118.600006 Q 70 118.90001 67.4 117.75001 Q 64.8 116.600006 63.1 114.200005 Q 61.4 112.00001 60.9 109.15001 Q 60.4 106.30001 60.8 101.700005 Q 61.1 97.8 63.3 93.850006 Q 65.5 89.90001 68.65 86.8 Q 71.8 83.70001 74.9 82.50001 Q 76.4 81.50001 77.45 81.55 Q 78.5 81.600006 80.7 81.8 Q 83 82.100006 84.15 82.70001 Q 85.3 83.3 86 84.3 Q 87.5 85.8 88 87.25001 Q 88.5 88.700005 88.5 90.100006 Q 88.100006 92.3 85.55 95.450005 Q 83 98.600006 78.9 100.600006 Q 76.7 101.3 74.1 101.200005 Q 71.5 101.100006 69.8 99.90001 Q 67.9 99.100006 67.3 100.15001 Q 66.7 101.200005 66.2 105.200005 Q 65.8 109.00001 66.8 110.75001 Q 67.8 112.50001 69.5 112.80001 Q 71.5 113.200005 72.65 113.350006 Q 73.8 113.50001 75.5 112.50001 Q 77.1 111.50001 77.5 111.350006 Q 77.9 111.200005 78.15 110.950005 Q 78.4 110.700005 79.4 109.700005 Q 80.1 109.00001 80.5 108.65001 Q 80.9 108.30001 81.4 108.30001 Q 81.9 108.30001 82.3 108.75001 Q 82.7 109.200005 83.100006 109.40001 Q 84.100006 109.40001 83.55 110.600006 Q 83 111.80001 81.4 113.450005 Q 79.8 115.100006 77.6 116.50001 ZM 77.5 95.3 Q 78.6 94.8 79.8 93.850006 Q 81 92.90001 81.85 91.75001 Q 82.7 90.600006 82.7 89.8 Q 82.7 88.40001 81.4 87.45001 Q 80.1 86.50001 77.5 87.20001 Q 76.4 87.50001 75 88.65001 Q 73.6 89.8 72.35 91.200005 Q 71.1 92.600006 70.35 93.700005 Q 69.6 94.8 69.8 95.00001 Q 70.1 95.50001 71.5 95.75001 Q 72.9 96.00001 74.65 95.90001 Q 76.4 95.8 77.5 95.3 Z"
|
||||
fill="currentColor"
|
||||
style={{ "--path-length": "243.02614" } as React.CSSProperties}
|
||||
/>
|
||||
<path
|
||||
d="M 103.4 118.90001 Q 100.5 119.40001 98.1 118.55001 Q 95.7 117.700005 94.25 115.65001 Q 92.8 113.600006 92.8 110.40001 Q 92.5 107.40001 93.5 103.450005 Q 94.5 99.50001 96.45 95.50001 Q 98.4 91.50001 100.7 88.600006 Q 102.7 86.20001 104.7 84.20001 Q 106.7 82.20001 108.3 81.50001 Q 109.7 80.90001 110.95 80.75001 Q 112.2 80.600006 113 80.600006 Q 114.600006 81.00001 116.45 82.05 Q 118.3 83.100006 118.3 84.3 Q 118.3 84.3 118.55 84.55 Q 118.8 84.8 118.8 84.8 Q 119.4 84.8 119.850006 86.100006 Q 120.3 87.40001 120.4 89.05 Q 120.5 90.700005 119.9 91.8 Q 119.5 93.700005 118.5 95.100006 Q 117.5 96.50001 116.7 96.50001 Q 116.3 96.50001 115.95 96.55 Q 115.600006 96.600006 115.600006 97.100006 Q 115.600006 97.600006 115.05 97.50001 Q 114.5 97.40001 113.9 96.850006 Q 113.3 96.3 113 95.50001 Q 113 95.00001 113.4 93.8 Q 113.8 92.600006 114.2 90.700005 Q 114.8 87.90001 114.1 87.05 Q 113.4 86.20001 111.4 87.00001 Q 108.9 88.40001 106.65 90.90001 Q 104.4 93.40001 102.5 97.350006 Q 100.6 101.3 99.1 107.200005 Q 98.5 109.50001 98.65 111.200005 Q 98.8 112.90001 99.1 113.00001 Q 100.5 113.40001 102.9 113.15001 Q 105.3 112.90001 106.4 112.30001 Q 108 111.50001 110 110.15001 Q 112 108.80001 113.5 107.700005 Q 114 107.40001 114.7 106.700005 Q 115.4 106.00001 116.2 105.600006 Q 117 104.40001 117.600006 104.200005 Q 118.2 104.00001 119.4 104.00001 Q 120.2 104.200005 120.9 104.600006 Q 121.600006 105.00001 121.850006 105.40001 Q 122.100006 105.80001 121.5 106.100006 Q 121.100006 106.100006 121.05 106.350006 Q 121 106.600006 121 106.600006 Q 121.2 107.00001 120.15 108.25001 Q 119.100006 109.50001 117.5 111.00001 Q 115.9 112.50001 114.45 113.55001 Q 113 114.600006 112.4 114.600006 Q 111.4 114.600006 111.4 115.200005 Q 111.4 115.40001 109.95 116.200005 Q 108.5 117.00001 106.65 117.80001 Q 104.8 118.600006 103.4 118.90001 Z"
|
||||
fill="currentColor"
|
||||
style={{ "--path-length": "197.86472" } as React.CSSProperties}
|
||||
/>
|
||||
<path
|
||||
d="M 131.6 114.600006 Q 130.9 114.700005 130.15 114.55001 Q 129.4 114.40001 128.7 113.90001 Q 127.9 113.100006 127.6 112.65001 Q 127.3 112.200005 127.45 111.200005 Q 127.6 110.200005 128.1 108.100006 Q 128.6 106.50001 129.3 103.65001 Q 130 100.8 130.8 98.00001 Q 131.1 96.40001 131.65 94.700005 Q 132.2 93.00001 132.8 91.55 Q 133.4 90.100006 133.7 89.3 Q 134.5 86.50001 135.25 84.40001 Q 136 82.3 137.1 79.55 Q 138.2 76.8 139.8 72.20001 Q 142 66.20001 143.15 63.050007 Q 144.3 59.900005 144.85 58.45001 Q 145.4 57.000008 145.6 56.20001 Q 145.6 55.40001 146.05 55.000008 Q 146.5 54.600006 147.2 54.600006 Q 148.6 54.600006 149.45 55.950005 Q 150.3 57.300007 150.5 59.150005 Q 150.7 61.000008 149.9 62.600006 Q 148.3 66.20001 146.5 71.15001 Q 144.7 76.100006 143 80.850006 Q 141.3 85.600006 139.8 88.90001 Q 139.8 89.3 139.5 90.3 Q 139.2 91.3 138.8 92.100006 Q 138.4 92.90001 138.05 93.600006 Q 137.7 94.3 137.7 94.3 Q 137.7 94.700005 137.4 95.8 Q 137.1 96.90001 136.6 98.50001 Q 136.2 99.700005 135.65 101.600006 Q 135.1 103.50001 134.9 104.80001 Q 135.4 103.90001 137 102.100006 Q 138.6 100.3 140 99.00001 Q 141.6 97.600006 143.6 95.90001 Q 145.6 94.200005 146.8 93.40001 Q 148 92.600006 148.95 91.950005 Q 149.9 91.3 149.9 91.3 Q 149.9 90.90001 150.25 90.850006 Q 150.6 90.8 151 90.8 Q 151.4 90.8 151.45 90.700005 Q 151.5 90.600006 151.5 90.200005 Q 151.5 89.700005 152.65 89.3 Q 153.8 88.90001 155.35 88.65001 Q 156.9 88.40001 157.9 88.40001 Q 159.1 88.40001 159.8 88.90001 Q 160.5 89.40001 161.6 91.100006 Q 162.8 92.700005 163.15 94.950005 Q 163.5 97.200005 162.7 99.600006 Q 162.7 100.40001 162.45 101.850006 Q 162.2 103.3 162.2 104.90001 L 161.6 108.100006 L 163.2 108.100006 Q 164 108.100006 164.7 108.350006 Q 165.4 108.600006 165.4 108.600006 Q 165.8 109.00001 166.2 109.350006 Q 166.6 109.700005 167 109.700005 Q 168.2 109.700005 167.9 110.65001 Q 167.6 111.600006 166.4 112.40001 Q 164 114.00001 162.05 114.00001 Q 160.1 114.00001 158.5 112.40001 Q 156.9 110.700005 156.5 108.600006 Q 156.1 106.50001 156.9 103.3 Q 157.3 98.90001 157.35 96.850006 Q 157.4 94.8 156.3 94.8 Q 155.3 94.8 152.8 96.15001 Q 150.3 97.50001 147.2 99.8 Q 144.1 102.100006 141.1 104.90001 Q 138.1 107.700005 136.1 110.700005 Q 134.9 112.200005 133.8 113.350006 Q 132.7 114.50001 131.6 114.600006 Z"
|
||||
fill="currentColor"
|
||||
style={{ "--path-length": "260.79596" } as React.CSSProperties}
|
||||
/>
|
||||
<path
|
||||
d="M 181.29999 118.50001 Q 180 118.50001 179.25 118.00001 Q 178.5 117.50001 177.4 116.600006 Q 175.5 114.600006 174.7 112.75001 Q 173.9 110.90001 174.2 109.200005 Q 174.5 108.40001 174.45 107.65001 Q 174.4 106.90001 174.4 106.90001 Q 173.9 106.40001 174.09999 106.25001 Q 174.29999 106.100006 174.5 105.700005 Q 175.09999 105.700005 174.9 104.600006 Q 174.7 103.8 175.7 101.100006 Q 176.7 98.40001 178.2 96.00001 Q 178.79999 95.00001 180 93.350006 Q 181.2 91.700005 182.65 90.05 Q 184.09999 88.40001 185.29999 87.40001 Q 187.79999 85.3 189.7 84.3 Q 191.59999 83.3 193.79999 82.8 Q 196 82.3 196.84999 82.3 Q 197.7 82.3 199.2 83.90001 Q 201.5 84.90001 202.54999 86.3 Q 203.59999 87.70001 203.9 90.40001 Q 204.2 93.40001 203.45 97.25001 Q 202.7 101.100006 198.7 106.80001 Q 196.2 110.40001 194.09999 112.15001 Q 192 113.90001 189.09999 115.600006 Q 187 117.00001 185.29999 117.65001 Q 183.59999 118.30001 181.29999 118.50001 ZM 190.9 107.90001 Q 193.29999 105.200005 194.75 102.8 Q 196.2 100.40001 197.5 96.700005 Q 197.9 95.600006 198 93.950005 Q 198.09999 92.3 197.95 90.850006 Q 197.79999 89.40001 197.2 89.200005 Q 196 88.50001 194.29999 88.75001 Q 192.59999 89.00001 190.59999 90.700005 Q 187.5 93.3 185.2 97.15001 Q 182.9 101.00001 180.7 105.90001 Q 179.2 109.50001 180.04999 111.65001 Q 180.9 113.80001 183.75 113.15001 Q 186.59999 112.50001 190.9 107.90001 Z"
|
||||
fill="currentColor"
|
||||
style={{ "--path-length": "217.35109" } as React.CSSProperties}
|
||||
/>
|
||||
<path
|
||||
d="M 227.3 116.50001 Q 225.2 118.30001 222.45 118.600006 Q 219.7 118.90001 217.09999 117.75001 Q 214.5 116.600006 212.8 114.200005 Q 211.09999 112.00001 210.59999 109.15001 Q 210.09999 106.30001 210.5 101.700005 Q 210.8 97.8 213 93.850006 Q 215.2 89.90001 218.34999 86.8 Q 221.5 83.70001 224.59999 82.50001 Q 226.09999 81.50001 227.15 81.55 Q 228.2 81.600006 230.4 81.8 Q 232.7 82.100006 233.85 82.70001 Q 235 83.3 235.7 84.3 Q 237.2 85.8 237.7 87.25001 Q 238.2 88.700005 238.2 90.100006 Q 237.8 92.3 235.25 95.450005 Q 232.7 98.600006 228.59999 100.600006 Q 226.4 101.3 223.8 101.200005 Q 221.2 101.100006 219.5 99.90001 Q 217.59999 99.100006 217 100.15001 Q 216.4 101.200005 215.9 105.200005 Q 215.5 109.00001 216.5 110.75001 Q 217.5 112.50001 219.2 112.80001 Q 221.2 113.200005 222.34999 113.350006 Q 223.5 113.50001 225.2 112.50001 Q 226.8 111.50001 227.2 111.350006 Q 227.59999 111.200005 227.84999 110.950005 Q 228.09999 110.700005 229.09999 109.700005 Q 229.8 109.00001 230.2 108.65001 Q 230.59999 108.30001 231.09999 108.30001 Q 231.59999 108.30001 232 108.75001 Q 232.4 109.200005 232.8 109.40001 Q 233.8 109.40001 233.25 110.600006 Q 232.7 111.80001 231.09999 113.450005 Q 229.5 115.100006 227.3 116.50001 ZM 227.2 95.3 Q 228.3 94.8 229.5 93.850006 Q 230.7 92.90001 231.55 91.75001 Q 232.4 90.600006 232.4 89.8 Q 232.4 88.40001 231.09999 87.45001 Q 229.8 86.50001 227.2 87.20001 Q 226.09999 87.50001 224.7 88.65001 Q 223.3 89.8 222.05 91.200005 Q 220.8 92.600006 220.05 93.700005 Q 219.3 94.8 219.5 95.00001 Q 219.8 95.50001 221.2 95.75001 Q 222.59999 96.00001 224.34999 95.90001 Q 226.09999 95.8 227.2 95.3 Z"
|
||||
fill="currentColor"
|
||||
style={{ "--path-length": "243.02614" } as React.CSSProperties}
|
||||
/>
|
||||
<path
|
||||
d="M 248.3 120.40001 Q 246.4 119.600006 244.7 118.25001 Q 243 116.90001 242.5 116.00001 Q 242.5 115.100006 242.8 114.75001 Q 243.09999 114.40001 244.55 114.450005 Q 246 114.50001 249.3 114.90001 Q 250.5 114.90001 252.3 114.700005 Q 254.09999 114.50001 255.65 114.100006 Q 257.2 113.700005 257.4 113.100006 Q 257.9 113.100006 257.6 112.75001 Q 257.3 112.40001 256.9 112.00001 Q 256.5 112.00001 256 111.65001 Q 255.5 111.30001 255.09999 111.30001 Q 255.09999 110.80001 254.15 110.15001 Q 253.2 109.50001 252.2 109.100006 Q 250 107.30001 248.4 105.850006 Q 246.8 104.40001 247.2 103.90001 Q 247.2 103.50001 247.2 103.450005 Q 247.2 103.40001 247.2 103.40001 Q 246.5 103.40001 245.65 101.40001 Q 244.8 99.40001 244.8 97.8 Q 244.8 96.600006 245.5 94.75001 Q 246.2 92.90001 247.4 90.90001 Q 248.59999 88.90001 249.9 87.600006 Q 251.7 85.90001 254.4 83.95001 Q 257.1 82.00001 259.75 80.45001 Q 262.4 78.90001 264.2 78.40001 Q 266 77.70001 267.55 77.70001 Q 269.1 77.70001 270.3 78.40001 Q 271.3 78.90001 272.15 79.850006 Q 273 80.8 273.65 81.75001 Q 274.3 82.70001 274.5 83.40001 Q 274.5 84.600006 274.3 86.05 Q 274.1 87.50001 273.75 88.600006 Q 273.4 89.700005 272.7 89.700005 Q 272.7 89.700005 272.45 89.75001 Q 272.2 89.8 272.2 90.3 Q 272.2 91.3 270.7 92.600006 Q 268.8 93.90001 267.4 94.15001 Q 266 94.40001 265.7 93.100006 Q 265.4 92.700005 265.3 92.40001 Q 265.2 92.100006 264.8 92.100006 Q 264.8 92.100006 264.5 91.8 Q 264.2 91.50001 264.2 90.8 Q 264.2 90.40001 264.5 90.05 Q 264.8 89.700005 264.8 89.700005 Q 265.3 89.700005 266.1 88.55 Q 266.9 87.40001 267.55 86.05 Q 268.2 84.70001 268.2 84.20001 Q 268.2 83.600006 268.05 83.50001 Q 267.9 83.40001 267.1 83.40001 Q 265.7 84.00001 263.7 85.00001 Q 261.7 86.00001 259.95 86.95001 Q 258.2 87.90001 257.4 88.700005 Q 257 89.40001 256.9 89.55 Q 256.8 89.700005 256.4 89.700005 Q 256.4 89.3 255.75 90.05 Q 255.09999 90.8 254.15 92.100006 Q 253.2 93.40001 252.34999 94.65001 Q 251.5 95.90001 251.09999 96.50001 Q 250.9 96.700005 250.65 97.65001 Q 250.4 98.600006 250.4 99.40001 Q 250 100.600006 250.5 101.05 Q 251 101.50001 252.7 102.90001 Q 256.2 105.50001 258.2 107.200005 Q 260.2 108.90001 261.2 110.00001 Q 262.2 111.100006 262.4 112.00001 Q 264.2 115.00001 263.5 116.600006 Q 262.8 118.200005 260.1 119.40001 Q 259.1 119.90001 256.8 120.200005 Q 254.5 120.50001 252.09999 120.55001 Q 249.7 120.600006 248.3 120.40001 Z"
|
||||
fill="currentColor"
|
||||
style={{ "--path-length": "200.88203" } as React.CSSProperties}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -30,7 +30,7 @@ const ErrorPage = () => {
|
||||
抱歉,您访问的页面已经离家出走了
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
onClick={() => (window.location.href = "/")}
|
||||
className="mt-8 px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors duration-300"
|
||||
>
|
||||
返回首页
|
||||
|
@ -26,7 +26,9 @@ export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 flex flex-col items-center justify-center bg-black/25 dark:bg-black/40 z-[999999]">
|
||||
<div className="loading-spinner mb-2" />
|
||||
<div className="text-custom-p-light dark:text-custom-p-dark text-sm">加载中...</div>
|
||||
<div className="text-custom-p-light dark:text-custom-p-dark text-sm">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<style>{`
|
||||
|
@ -1,13 +1,17 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useState, useContext } from 'react';
|
||||
import { Button, Flex, Card, Text, Box } from '@radix-ui/themes';
|
||||
import { CheckCircledIcon, CrossCircledIcon, InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import React, { createContext, useState, useContext } from "react";
|
||||
import { Button, Flex, Card, Text, Box } from "@radix-ui/themes";
|
||||
import {
|
||||
CheckCircledIcon,
|
||||
CrossCircledIcon,
|
||||
InfoCircledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
|
||||
// 定义通知类型枚举
|
||||
export enum NotificationType {
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
INFO = 'info'
|
||||
SUCCESS = "success",
|
||||
ERROR = "error",
|
||||
INFO = "info",
|
||||
}
|
||||
|
||||
// 通知类型定义
|
||||
@ -28,16 +32,16 @@ type NotificationConfig = {
|
||||
const notificationConfigs: Record<NotificationType, NotificationConfig> = {
|
||||
[NotificationType.SUCCESS]: {
|
||||
icon: <CheckCircledIcon className="w-5 h-5 text-white" />,
|
||||
bgColor: 'bg-[rgba(0,168,91,0.85)]'
|
||||
bgColor: "bg-[rgba(0,168,91,0.85)]",
|
||||
},
|
||||
[NotificationType.ERROR]: {
|
||||
icon: <CrossCircledIcon className="w-5 h-5 text-white" />,
|
||||
bgColor: 'bg-[rgba(225,45,57,0.85)]'
|
||||
bgColor: "bg-[rgba(225,45,57,0.85)]",
|
||||
},
|
||||
[NotificationType.INFO]: {
|
||||
icon: <InfoCircledIcon className="w-5 h-5 text-white" />,
|
||||
bgColor: 'bg-[rgba(38,131,255,0.85)]'
|
||||
}
|
||||
bgColor: "bg-[rgba(38,131,255,0.85)]",
|
||||
},
|
||||
};
|
||||
|
||||
// 修改通知上下文类型定义
|
||||
@ -63,7 +67,9 @@ export const toast: NotificationContextType = {
|
||||
info: () => {},
|
||||
};
|
||||
|
||||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
|
||||
// 统一参数顺序:title 在前,message 在后
|
||||
@ -71,19 +77,24 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
const newNotification = { id, type, title, message };
|
||||
|
||||
setNotifications(prev => [...prev, newNotification]);
|
||||
setNotifications((prev) => [...prev, newNotification]);
|
||||
|
||||
setTimeout(() => {
|
||||
setNotifications(prev => prev.filter(notification => notification.id !== id));
|
||||
setNotifications((prev) =>
|
||||
prev.filter((notification) => notification.id !== id),
|
||||
);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 简化快捷方法定义
|
||||
const contextValue = {
|
||||
show,
|
||||
success: (title: string, message?: string) => show(NotificationType.SUCCESS, title, message),
|
||||
error: (title: string, message?: string) => show(NotificationType.ERROR, title, message),
|
||||
info: (title: string, message?: string) => show(NotificationType.INFO, title, message),
|
||||
success: (title: string, message?: string) =>
|
||||
show(NotificationType.SUCCESS, title, message),
|
||||
error: (title: string, message?: string) =>
|
||||
show(NotificationType.ERROR, title, message),
|
||||
info: (title: string, message?: string) =>
|
||||
show(NotificationType.INFO, title, message),
|
||||
};
|
||||
|
||||
// 初始化全局方法
|
||||
@ -92,7 +103,9 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
}, []);
|
||||
|
||||
const closeNotification = (id: string) => {
|
||||
setNotifications(prev => prev.filter(notification => notification.id !== id));
|
||||
setNotifications((prev) =>
|
||||
prev.filter((notification) => notification.id !== id),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -103,7 +116,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
top="4"
|
||||
className="fixed top-4 right-4 z-[1000] flex flex-col gap-2 w-full max-w-[360px] px-4 md:px-0 md:right-6"
|
||||
>
|
||||
{notifications.map(notification => (
|
||||
{notifications.map((notification) => (
|
||||
<Card
|
||||
key={notification.id}
|
||||
className="p-0 overflow-hidden shadow-lg w-full"
|
||||
@ -135,10 +148,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Text
|
||||
size="2"
|
||||
className="text-white/80 leading-normal"
|
||||
>
|
||||
<Text size="2" className="text-white/80 leading-normal">
|
||||
{notification.message}
|
||||
</Text>
|
||||
</Flex>
|
||||
@ -146,7 +156,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
<div
|
||||
className="h-full bg-white/20 animate-[progress_3s_linear]"
|
||||
style={{
|
||||
transformOrigin: 'left'
|
||||
transformOrigin: "left",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -164,7 +174,9 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
export const useNotification = () => {
|
||||
const context = useContext(NotificationContext);
|
||||
if (!context) {
|
||||
throw new Error('useNotification must be used within a NotificationProvider');
|
||||
throw new Error(
|
||||
"useNotification must be used within a NotificationProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
@ -1,27 +1,29 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "@radix-ui/themes";
|
||||
|
||||
const THEME_KEY = "theme-preference";
|
||||
|
||||
export const ThemeModeToggle: React.FC = () => {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const saved = localStorage.getItem(THEME_KEY);
|
||||
const initialTheme =
|
||||
saved ||
|
||||
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light");
|
||||
setIsDark(initialTheme === "dark");
|
||||
if (saved) {
|
||||
setIsDark(saved === "dark");
|
||||
document.documentElement.classList.toggle("dark", saved === "dark");
|
||||
document.documentElement.className = saved;
|
||||
} else {
|
||||
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
setIsDark(systemDark);
|
||||
document.documentElement.classList.toggle("dark", systemDark);
|
||||
document.documentElement.className = initialTheme;
|
||||
}
|
||||
|
||||
// 添加滚动监听
|
||||
let lastScroll = 0;
|
||||
const handleScroll = () => {
|
||||
const currentScroll = window.scrollY;
|
||||
@ -29,20 +31,16 @@ export const ThemeModeToggle: React.FC = () => {
|
||||
lastScroll = currentScroll;
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newIsDark = !isDark;
|
||||
setIsDark(newIsDark);
|
||||
localStorage.setItem(THEME_KEY, newIsDark ? "dark" : "light");
|
||||
document.documentElement.classList.toggle("dark", newIsDark);
|
||||
|
||||
const event = new CustomEvent("theme-change", {
|
||||
detail: { theme: newIsDark ? "dark" : "light" },
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
const newTheme = newIsDark ? "dark" : "light";
|
||||
document.documentElement.className = newTheme;
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
};
|
||||
|
||||
if (!mounted) return null;
|
||||
@ -52,15 +50,50 @@ export const ThemeModeToggle: React.FC = () => {
|
||||
variant="ghost"
|
||||
onClick={toggleTheme}
|
||||
className={`p-2 rounded-lg transition-all duration-300 transform ${
|
||||
visible ? 'translate-y-0 opacity-100' : '-translate-y-full opacity-0'
|
||||
visible ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0"
|
||||
}`}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{isDark ? (
|
||||
<SunIcon width="24" height="24" className="text-yellow-400" />
|
||||
<SunIcon width="24" height="24" />
|
||||
) : (
|
||||
<MoonIcon width="24" height="24" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const useThemeMode = () => {
|
||||
const [mode, setMode] = useState<"light" | "dark">("light");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem(THEME_KEY);
|
||||
if (saved) {
|
||||
setMode(saved as "light" | "dark");
|
||||
} else {
|
||||
const isDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
setMode(isDark ? "dark" : "light");
|
||||
}
|
||||
|
||||
// 监听主题变化事件
|
||||
const handleThemeChange = (e: CustomEvent) => {
|
||||
setMode(e.detail.theme);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"theme-change",
|
||||
handleThemeChange as EventListener,
|
||||
);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
"theme-change",
|
||||
handleThemeChange as EventListener,
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { mode };
|
||||
};
|
||||
|
239
frontend/hooks/tide.tsx
Normal file
239
frontend/hooks/tide.tsx
Normal file
@ -0,0 +1,239 @@
|
||||
"use client";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
const Tide: React.FC = () => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const dimensionsRef = useRef({ width: 1000, height: 800 });
|
||||
const pathCountRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
dimensionsRef.current = {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
|
||||
if (svgRef.current) {
|
||||
svgRef.current.setAttribute(
|
||||
"viewBox",
|
||||
`0 0 ${dimensionsRef.current.width} ${dimensionsRef.current.height}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createLine = (
|
||||
startX: number,
|
||||
startY: number,
|
||||
endX: number,
|
||||
endY: number,
|
||||
width: number,
|
||||
alpha: number = 0.3,
|
||||
animationDelay: number = 0,
|
||||
) => {
|
||||
if (!svgRef.current || pathCountRef.current > 500) return;
|
||||
|
||||
const path = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"path",
|
||||
);
|
||||
|
||||
const midX = (startX + endX) / 2;
|
||||
const midY = (startY + endY) / 2;
|
||||
const controlX = midX + (Math.random() - 0.5) * 2;
|
||||
const controlY = midY + (Math.random() - 0.5) * 2;
|
||||
|
||||
const d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${endX} ${endY}`;
|
||||
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("stroke", "var(--accent-9)");
|
||||
path.setAttribute("stroke-width", "1");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
path.setAttribute("fill", "none");
|
||||
|
||||
const length = path.getTotalLength();
|
||||
path.style.strokeDasharray = `${length}`;
|
||||
path.style.strokeDashoffset = `${length}`;
|
||||
path.style.opacity = "0";
|
||||
path.style.transition = `
|
||||
stroke-dashoffset 0.8s ease-out ${animationDelay}s,
|
||||
opacity 0.8s ease-out ${animationDelay}s
|
||||
`;
|
||||
|
||||
svgRef.current.appendChild(path);
|
||||
pathCountRef.current += 1;
|
||||
|
||||
setTimeout(() => {
|
||||
path.style.strokeDashoffset = "0";
|
||||
path.style.opacity = "0.6";
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const createRoot = (
|
||||
startX: number,
|
||||
startY: number,
|
||||
baseAngle: number,
|
||||
length: number,
|
||||
width: number,
|
||||
depth: number,
|
||||
animationDelay: number = 0,
|
||||
) => {
|
||||
if (depth <= 0 || !svgRef.current || pathCountRef.current > 600) return;
|
||||
|
||||
const endX = startX + Math.cos(baseAngle) * length;
|
||||
const endY = startY - Math.sin(baseAngle) * length;
|
||||
|
||||
if (
|
||||
endX < 0 ||
|
||||
endX > dimensionsRef.current.width ||
|
||||
endY < 0 ||
|
||||
endY > dimensionsRef.current.height
|
||||
)
|
||||
return;
|
||||
|
||||
createLine(startX, startY, endX, endY, width, 0.6, animationDelay);
|
||||
|
||||
const growthDelay = 0.3;
|
||||
const newDelay = animationDelay + growthDelay;
|
||||
|
||||
setTimeout(() => {
|
||||
if (depth > 0) {
|
||||
createRoot(
|
||||
endX,
|
||||
endY,
|
||||
baseAngle + (Math.random() * 0.08 - 0.04),
|
||||
length * 0.99,
|
||||
width * 0.99,
|
||||
depth - 1,
|
||||
newDelay,
|
||||
);
|
||||
}
|
||||
|
||||
const branchProbability = depth > 20 ? 0.3 : 0.2;
|
||||
|
||||
if (depth > 5 && depth < 35 && Math.random() < branchProbability) {
|
||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||
const branchAngle =
|
||||
baseAngle +
|
||||
direction * (Math.PI / 6 + (Math.random() * Math.PI) / 12);
|
||||
|
||||
setTimeout(() => {
|
||||
createRoot(
|
||||
endX,
|
||||
endY,
|
||||
branchAngle,
|
||||
length * 0.85,
|
||||
width * 0.85,
|
||||
Math.floor(depth * 0.8),
|
||||
newDelay + 0.2,
|
||||
);
|
||||
}, 150);
|
||||
}
|
||||
}, growthDelay * 1000);
|
||||
};
|
||||
|
||||
const startGrowth = () => {
|
||||
if (!svgRef.current) return;
|
||||
svgRef.current.innerHTML = "";
|
||||
pathCountRef.current = 0;
|
||||
updateDimensions();
|
||||
|
||||
const { width, height } = dimensionsRef.current;
|
||||
|
||||
const edge = Math.floor(Math.random() * 4);
|
||||
let startX, startY, baseAngle;
|
||||
|
||||
const margin = 50;
|
||||
const randomPos = Math.random();
|
||||
|
||||
switch (edge) {
|
||||
case 0:
|
||||
startX = margin + (width - 2 * margin) * randomPos;
|
||||
startY = 0;
|
||||
baseAngle = Math.PI / 2;
|
||||
break;
|
||||
case 1:
|
||||
startX = width;
|
||||
startY = margin + (height - 2 * margin) * randomPos;
|
||||
baseAngle = Math.PI;
|
||||
break;
|
||||
case 2:
|
||||
startX = margin + (width - 2 * margin) * randomPos;
|
||||
startY = height;
|
||||
baseAngle = -Math.PI / 2;
|
||||
break;
|
||||
default:
|
||||
startX = 0;
|
||||
startY = margin + (height - 2 * margin) * randomPos;
|
||||
baseAngle = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
const angleVariation = Math.random() * 0.4 - 0.2;
|
||||
|
||||
const minDepth = 25;
|
||||
const maxDepth = 45;
|
||||
const depth =
|
||||
minDepth + Math.floor(Math.random() * (maxDepth - minDepth));
|
||||
|
||||
const initialLength = 15 + Math.random() * 5;
|
||||
const initialWidth = 0.8 + Math.random() * 0.4;
|
||||
|
||||
createRoot(
|
||||
startX,
|
||||
startY,
|
||||
baseAngle + angleVariation,
|
||||
initialLength,
|
||||
initialWidth,
|
||||
depth,
|
||||
0,
|
||||
);
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateDimensions();
|
||||
startGrowth();
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
setTimeout(startGrowth, 100);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999,
|
||||
pointerEvents: "none",
|
||||
background: "transparent",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tide;
|
@ -1,6 +1,6 @@
|
||||
import { HttpClient } from 'core/http';
|
||||
import { CapabilityService } from 'core/capability';
|
||||
import { Serializable } from 'interface/serializableType';
|
||||
import { HttpClient } from "core/http";
|
||||
import { CapabilityService } from "core/capability";
|
||||
import { Serializable } from "interface/serializableType";
|
||||
|
||||
export class Layout {
|
||||
private http: HttpClient;
|
||||
@ -14,16 +14,13 @@ export class Layout {
|
||||
services?: {
|
||||
http?: HttpClient;
|
||||
capability?: CapabilityService;
|
||||
}
|
||||
},
|
||||
) {
|
||||
this.http = services?.http || HttpClient.getInstance();
|
||||
this.capability = services?.capability || CapabilityService.getInstance();
|
||||
}
|
||||
|
||||
render(props: {
|
||||
children: React.ReactNode;
|
||||
args?: Serializable;
|
||||
}) {
|
||||
render(props: { children: React.ReactNode; args?: Serializable }) {
|
||||
return this.element(props);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { HttpClient } from 'core/http';
|
||||
import { CapabilityService } from 'core/capability';
|
||||
import { Serializable } from 'interface/serializableType';
|
||||
import { HttpClient } from "core/http";
|
||||
import { CapabilityService } from "core/capability";
|
||||
import { Serializable } from "interface/serializableType";
|
||||
|
||||
export class Template {
|
||||
constructor(
|
||||
@ -14,7 +14,7 @@ export class Template {
|
||||
http: HttpClient;
|
||||
capability: CapabilityService;
|
||||
args: Serializable;
|
||||
}) => React.ReactNode
|
||||
}) => React.ReactNode,
|
||||
) {}
|
||||
|
||||
render(services: {
|
||||
|
@ -14,7 +14,7 @@ export interface ThemeConfig {
|
||||
};
|
||||
configuration: Configuration;
|
||||
routes: {
|
||||
article:string;
|
||||
article: string;
|
||||
post: string;
|
||||
tag: string;
|
||||
category: string;
|
||||
|
@ -12,6 +12,7 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/themes": "^3.1.6",
|
||||
"@remix-run/node": "^2.14.0",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 86 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 168 KiB |
@ -3,7 +3,7 @@ import path from "path";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
// 设置全局最大监听器数量
|
||||
EventEmitter.defaultMaxListeners = 20;
|
||||
// EventEmitter.defaultMaxListeners = 20;
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@ -23,8 +23,8 @@ const startServers = async () => {
|
||||
});
|
||||
|
||||
// 等待内部服务器启动
|
||||
console.log("等待内部服务器启动...");
|
||||
await delay(2000);
|
||||
// console.log("等待内部服务器启动...");
|
||||
// await delay(2000);
|
||||
|
||||
// 然后启动 Vite
|
||||
const viteProcess = spawn("npm", ["run", "dev"], {
|
||||
|
@ -6,8 +6,10 @@ export default {
|
||||
"./common/**/*.{js,jsx,ts,tsx}",
|
||||
"./core/**/*.{js,jsx,ts,tsx}",
|
||||
"./hooks/**/*.{js,jsx,ts,tsx}",
|
||||
"./themes/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
darkMode: ["class", '[data-theme="dark"]'],
|
||||
important: true,
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
@ -15,13 +17,13 @@ export default {
|
||||
},
|
||||
keyframes: {
|
||||
progress: {
|
||||
from: { transform: 'scaleX(1)' },
|
||||
to: { transform: 'scaleX(0)' }
|
||||
}
|
||||
from: { transform: "scaleX(1)" },
|
||||
to: { transform: "scaleX(0)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
progress: 'progress 3s linear'
|
||||
}
|
||||
progress: "progress 3s linear",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Config;
|
||||
|
@ -1,15 +1,10 @@
|
||||
import { Template } from 'interface/template';
|
||||
import { Template } from "interface/template";
|
||||
|
||||
export default new Template(
|
||||
{
|
||||
layout: "default",
|
||||
},
|
||||
({ http,args }) => {
|
||||
return (
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
);
|
||||
({ http, args }) => {
|
||||
return <div>Hello World</div>;
|
||||
},
|
||||
|
||||
);
|
@ -1,86 +1,135 @@
|
||||
import { Layout } from "interface/layout";
|
||||
import { ThemeModeToggle } from "hooks/themeMode";
|
||||
import { Echoes } from "hooks/echo";
|
||||
import { Container, Flex, Box, Link } from "@radix-ui/themes";
|
||||
import { Echoes } from "hooks/echoes";
|
||||
import Tide from "hooks/tide";
|
||||
import {
|
||||
Container,
|
||||
Flex,
|
||||
Box,
|
||||
Link,
|
||||
TextField,
|
||||
DropdownMenu,
|
||||
} from "@radix-ui/themes";
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
HamburgerMenuIcon,
|
||||
Cross1Icon,
|
||||
PersonIcon,
|
||||
CheckIcon,
|
||||
AvatarIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Theme } from "@radix-ui/themes";
|
||||
import { useState } from "react";
|
||||
|
||||
export default new Layout(({ children, args }) => {
|
||||
const [moreState, setMoreState] = useState(false);
|
||||
const [loginState, setLoginState] = useState(false);
|
||||
return (
|
||||
<Box className="min-h-screen flex flex-col">
|
||||
{/* 导航栏 */}
|
||||
<Box asChild className="fixed top-0 w-full border-b backdrop-blur-sm z-50">
|
||||
<nav>
|
||||
<Container size="4" className="mx-auto">
|
||||
<Flex justify="between" align="center" className="h-16">
|
||||
<Theme
|
||||
grayColor="gray"
|
||||
accentColor="gray"
|
||||
radius="medium"
|
||||
panelBackground="solid"
|
||||
>
|
||||
<Box className="min-h-screen flex flex-col">
|
||||
{/* 导航栏 */}
|
||||
<Box
|
||||
asChild
|
||||
className="fixed top-0 w-full backdrop-blur-sm border-b border-[--gray-a5] z-50"
|
||||
id="nav"
|
||||
>
|
||||
<Container size="4">
|
||||
<Flex justify="between" align="center" className="h-16 px-4">
|
||||
{/* Logo 区域 */}
|
||||
<Flex align="center" gap="4">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl font-bold flex items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Echoes/>
|
||||
<Flex align="center">
|
||||
<Link href="/" className="flex items-center">
|
||||
<Box className="w-20 h-20">
|
||||
<Echoes />
|
||||
</Box>
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
{/* 导航链接 */}
|
||||
<Flex align="center" gap="6">
|
||||
<Flex gap="4">
|
||||
<Link
|
||||
href="/posts"
|
||||
className="hover:opacity-80 transition-opacity font-medium"
|
||||
{/* 右侧导航链接 */}
|
||||
<Flex align="center" gap="5">
|
||||
{/* 桌面端搜索框和用户图标 */}
|
||||
<Box
|
||||
id="nav-desktop"
|
||||
className="hidden lg:flex items-center gap-5"
|
||||
>
|
||||
<TextField.Root
|
||||
size="2"
|
||||
variant="surface"
|
||||
placeholder="搜索..."
|
||||
className="w-[200px]"
|
||||
>
|
||||
文章
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="hover:opacity-80 transition-opacity font-medium"
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--accent-a11]" />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Box className="hover:opacity-70 transition-opacity p-2">
|
||||
{loginState ? (
|
||||
<AvatarIcon className="w-5 h-5 text-current opacity-70" />
|
||||
) : (
|
||||
<div>
|
||||
<PersonIcon className="w-5 h-5 text-current opacity-80" />
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</DropdownMenu.Trigger>
|
||||
</DropdownMenu.Root>
|
||||
</Box>
|
||||
|
||||
{/* 移动端菜单按钮和下拉搜索框 */}
|
||||
<Box id="nav-mobile" className="lg:hidden">
|
||||
<DropdownMenu.Root
|
||||
onOpenChange={() => setMoreState(!moreState)}
|
||||
>
|
||||
关于
|
||||
</Link>
|
||||
</Flex>
|
||||
<DropdownMenu.Trigger>
|
||||
<Box className="hover:opacity-70 transition-opacity p-2">
|
||||
{moreState ? (
|
||||
<Cross1Icon className="h-5 w-5 text-[--accent-a11]" />
|
||||
) : (
|
||||
<HamburgerMenuIcon className="h-5 w-5 text-[--accent-a11]" />
|
||||
)}
|
||||
</Box>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
align="end"
|
||||
className="mt-2 p-3 min-w-[250px]"
|
||||
>
|
||||
<TextField.Root
|
||||
size="2"
|
||||
variant="surface"
|
||||
placeholder="搜索..."
|
||||
>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--accent-a11]" />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Box>
|
||||
|
||||
{/* 主题切换按钮 */}
|
||||
<Box>
|
||||
<ThemeModeToggle />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
</nav>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<Box className="flex-1 w-full mt-16">
|
||||
<Container size="4" className="py-8">
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* 页脚 */}
|
||||
<Box asChild className="w-full border-t mt-auto">
|
||||
<footer>
|
||||
{/* 主要内容区域 */}
|
||||
<Box className="flex-1 w-full mt-16">
|
||||
<Container size="4" className="py-8">
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<Flex gap="6" className="text-sm">
|
||||
<Link href="/terms" className="hover:opacity-80 transition-opacity">
|
||||
使用条款
|
||||
</Link>
|
||||
<Link href="/privacy" className="hover:opacity-80 transition-opacity">
|
||||
隐私政策
|
||||
</Link>
|
||||
<Link href="/contact" className="hover:opacity-80 transition-opacity">
|
||||
联系我们
|
||||
</Link>
|
||||
</Flex>
|
||||
<Box className="text-sm text-center opacity-85">
|
||||
<p>© {new Date().getFullYear()} Echoes. All rights reserved.</p>
|
||||
<p className="mt-1 text-xs opacity-75">
|
||||
Powered by Echoes Framework
|
||||
</p>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Tide />
|
||||
<main>{children}</main>
|
||||
</Container>
|
||||
</footer>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Theme>
|
||||
);
|
||||
});
|
||||
|
@ -6,11 +6,9 @@ const themeConfig: ThemeConfig = {
|
||||
version: "1.0.0",
|
||||
description: "一个简约风格的博客主题",
|
||||
author: "lsy",
|
||||
configuration: {
|
||||
|
||||
},
|
||||
globalSettings:{
|
||||
layout:"layout.tsx"
|
||||
configuration: {},
|
||||
globalSettings: {
|
||||
layout: "layout.tsx",
|
||||
},
|
||||
templates: new Map([
|
||||
[
|
||||
@ -24,14 +22,12 @@ const themeConfig: ThemeConfig = {
|
||||
]),
|
||||
|
||||
routes: {
|
||||
article:"",
|
||||
article: "",
|
||||
post: "",
|
||||
tag: "",
|
||||
category: "",
|
||||
error: "",
|
||||
page: new Map<string, string>([
|
||||
|
||||
]),
|
||||
page: new Map<string, string>([]),
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user